Просмотр исходного кода

videoContentList 引入先验需求-场景第4池,后验需求严格只取昨日数据

- selectForRecommend 增加 driveDimensionTime 参数
- 新增 fetchPriorSceneCandidates:demand_strategy='先验需求-场景',按 total_rov DESC 直接取,不分组
- fetchPosteriorCandidates 强制 drive_dimension_time='昨日'
- getInterleavedPage 改为 4 池随机穿插,priorScene 与 prior 都对外标 source='prior'
- getSingleSourcePage(source=prior) 改为 priorScene+prior 严格 1:1 穿插
- 修复常量 PRIOR_PREMIUM_DIMENSION typo(传播头部 → 传播的头部)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
刘立冬 1 день назад
Родитель
Сommit
2860bdce51

+ 1 - 0
api-module/src/main/java/com/tzld/piaoquan/api/dao/mapper/contentplatform/ext/ContentPlatformDemandVideoMapperExt.java

@@ -41,6 +41,7 @@ public interface ContentPlatformDemandVideoMapperExt {
                                                        @Param("dimensionExclude") String dimensionExclude,
                                                        @Param("demandFilterSortStrategyLike") String demandFilterSortStrategyLike,
                                                        @Param("channelLevel3") String channelLevel3,
+                                                       @Param("driveDimensionTime") String driveDimensionTime,
                                                        @Param("limit") int limit,
                                                        @Param("excludeSelfTitle") boolean excludeSelfTitle);
 

+ 96 - 22
api-module/src/main/java/com/tzld/piaoquan/api/service/contentplatform/impl/ContentPlatformPlanServiceImpl.java

@@ -612,10 +612,12 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
     private static final int HOT_CANDIDATE_LIMIT = 10000;
     private static final int TOP_K_PER_DEMAND = 3;
     private static final String DEMAND_STRATEGY_PRIOR = "先验需求";
+    private static final String DEMAND_STRATEGY_PRIOR_SCENE = "先验需求-场景";
     private static final String DEMAND_STRATEGY_POSTERIOR = "后验需求";
-    private static final String PRIOR_PREMIUM_DIMENSION = "传播头部";
+    private static final String PRIOR_PREMIUM_DIMENSION = "传播头部";
     private static final String POSTERIOR_FILTER_ABS_LIKE = "绝对高效率%";
     private static final String POSTERIOR_FILTER_REL_LIKE = "相对裂变率%";
+    private static final String POSTERIOR_DRIVE_DIMENSION_TIME = "昨日";
     private static final String SOURCE_PRIOR = "prior";
     private static final String SOURCE_POSTERIOR = "posterior";
     private static final String SOURCE_HOT = "hot";
@@ -648,15 +650,48 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
         if (SOURCE_HOT.equals(source)) {
             return getHotSourcePaged(param, user);
         }
-        List<VideoContentItemVO> list = SOURCE_PRIOR.equals(source)
-                ? fetchPriorCandidates(param, user, DEMAND_CANDIDATE_LIMIT)
-                : fetchPosteriorCandidates(param, user, DEMAND_CANDIDATE_LIMIT);
+        List<VideoContentItemVO> list;
+        if (SOURCE_PRIOR.equals(source)) {
+            // 粉丝喜欢 = 先验需求-场景 与 先验需求 严格 1:1 穿插,场景先出,prior 用完顺位补齐
+            List<VideoContentItemVO> scene = fetchPriorSceneCandidates(param, user, DEMAND_CANDIDATE_LIMIT);
+            List<VideoContentItemVO> prior = fetchPriorCandidates(param, user, DEMAND_CANDIDATE_LIMIT);
+            list = interleavePriorWithScene(scene, prior);
+        } else {
+            list = fetchPosteriorCandidates(param, user, DEMAND_CANDIDATE_LIMIT);
+        }
         for (VideoContentItemVO v : list) {
             v.setSource(source);
         }
         return paginateCandidates(param, list);
     }
 
+    /**
+     * priorScene 与 prior 严格 1:1 穿插 + 跨池 video_id 去重(priorScene 优先到达)。
+     * 一侧用完后,另一侧剩余按原顺序追加。
+     */
+    private List<VideoContentItemVO> interleavePriorWithScene(List<VideoContentItemVO> scene, List<VideoContentItemVO> prior) {
+        Set<Long> seen = new HashSet<>();
+        List<VideoContentItemVO> out = new ArrayList<>();
+        int si = 0, pi = 0;
+        while (si < scene.size() || pi < prior.size()) {
+            while (si < scene.size()) {
+                VideoContentItemVO v = scene.get(si++);
+                if (v.getVideoId() != null && seen.add(v.getVideoId())) {
+                    out.add(v);
+                    break;
+                }
+            }
+            while (pi < prior.size()) {
+                VideoContentItemVO v = prior.get(pi++);
+                if (v.getVideoId() != null && seen.add(v.getVideoId())) {
+                    out.add(v);
+                    break;
+                }
+            }
+        }
+        return out;
+    }
+
     /**
      * 单源 hot:复用原 planMapperExt.getVideoCount + getVideoList 真分页链路。
      */
@@ -700,21 +735,25 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
     }
 
     /**
-     * 三路随机穿插 + 跨路 video_id 去重。
-     * 每步在未耗尽的池中等概率随机选一个,从该池头部取下一条(池内仍保持需求强度优先、组内 score DESC 的次序)。
-     * 用 (userId ^ 当天日期) 作为种子,保证同一用户当天翻页顺序一致、刷新一致。
+     * 四路随机穿插 + 跨路 video_id 去重。
+     * 4 池: priorScene / prior / posterior / hot,priorScene 与 prior 对外都标 source='prior'(粉丝喜欢)。
+     * 每步在未耗尽的池中等概率随机选一个,从该池头部取下一条(池内顺序由 fetcher 决定)。
+     * 用 (userId ^ 当天日期) 作为种子,保证同一用户当天翻页顺序一致、刷新一致。
      */
     private Page<VideoContentItemVO> getInterleavedPage(VideoContentListParam param, ContentPlatformAccount user) {
+        List<VideoContentItemVO> priorScene = fetchPriorSceneCandidates(param, user, DEMAND_CANDIDATE_LIMIT);
         List<VideoContentItemVO> prior = fetchPriorCandidates(param, user, DEMAND_CANDIDATE_LIMIT);
         List<VideoContentItemVO> posterior = fetchPosteriorCandidates(param, user, DEMAND_CANDIDATE_LIMIT);
         List<VideoContentItemVO> hot = fetchHotCandidates(param, user, HOT_CANDIDATE_LIMIT);
+        for (VideoContentItemVO v : priorScene) v.setSource(SOURCE_PRIOR);
         for (VideoContentItemVO v : prior) v.setSource(SOURCE_PRIOR);
         for (VideoContentItemVO v : posterior) v.setSource(SOURCE_POSTERIOR);
         for (VideoContentItemVO v : hot) v.setSource(SOURCE_HOT);
 
-        List<List<VideoContentItemVO>> pools = Arrays.asList(prior, posterior, hot);
-        int[] pointers = new int[3];
-        boolean[] exhausted = new boolean[3];
+        List<List<VideoContentItemVO>> pools = Arrays.asList(priorScene, prior, posterior, hot);
+        int N = pools.size();
+        int[] pointers = new int[N];
+        boolean[] exhausted = new boolean[N];
         Set<Long> emittedIds = new HashSet<>();
         List<VideoContentItemVO> merged = new ArrayList<>();
 
@@ -722,9 +761,15 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
         long seed = userSeed ^ LocalDate.now().toString().hashCode();
         Random rng = new Random(seed);
 
-        while (!(exhausted[0] && exhausted[1] && exhausted[2])) {
-            List<Integer> alive = new ArrayList<>(3);
-            for (int i = 0; i < 3; i++) {
+        while (true) {
+            boolean allExhausted = true;
+            for (boolean e : exhausted) {
+                if (!e) { allExhausted = false; break; }
+            }
+            if (allExhausted) break;
+
+            List<Integer> alive = new ArrayList<>(N);
+            for (int i = 0; i < N; i++) {
                 if (!exhausted[i]) alive.add(i);
             }
             int cur = alive.get(rng.nextInt(alive.size()));
@@ -759,6 +804,35 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
         return result;
     }
 
+    /**
+     * 先验需求-场景池: demand_strategy='先验需求-场景',直接按 total_rov DESC, score DESC 取 top N,不分组。
+     * 退化策略: ghName 非空且查不到数据 → 退回渠道粒度(不限 channel_level3)。
+     * 行内不过滤 rov<=0(场景型命中数据 rov 通常都有值,且这类数据稀缺,不再二次过滤)。
+     */
+    private List<VideoContentItemVO> fetchPriorSceneCandidates(VideoContentListParam param, ContentPlatformAccount user, int limit) {
+        String dt = demandVideoMapperExt.getMaxDt();
+        if (!StringUtils.hasText(dt)) {
+            return new ArrayList<>();
+        }
+        String crowdSegment = user.getChannel();
+        String ghName = StringUtils.hasText(param.getGhName()) ? param.getGhName() : null;
+
+        List<ContentPlatformDemandVideo> rows = demandVideoMapperExt.selectForRecommend(
+                dt, crowdSegment, DEMAND_STRATEGY_PRIOR_SCENE, null, null, null, ghName, null, limit, false);
+        if (ghName != null && rows.isEmpty()) {
+            rows = demandVideoMapperExt.selectForRecommend(
+                    dt, crowdSegment, DEMAND_STRATEGY_PRIOR_SCENE, null, null, null, null, null, limit, false);
+        }
+        List<ContentPlatformDemandVideo> filtered = new ArrayList<>(rows.size());
+        Set<Long> seen = new HashSet<>();
+        for (ContentPlatformDemandVideo r : rows) {
+            if (r.getVideoId() == null) continue;
+            if (!seen.add(r.getVideoId())) continue;
+            filtered.add(r);
+        }
+        return buildDemandVideoContentItemVOList(filtered);
+    }
+
     /**
      * 先验池:A 段 dimension='传播头部' → B 段 其余 dimension。
      * 每段按 (point_type, standard_element) 分组,组按 total_rov DESC、组内 score DESC 取前 K;段间拼接 + video_id 去重。
@@ -773,16 +847,16 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
         int fetchLimit = Math.max(limit * 3, DEMAND_CANDIDATE_LIMIT);
 
         List<ContentPlatformDemandVideo> stageA = demandVideoMapperExt.selectForRecommend(
-                dt, crowdSegment, DEMAND_STRATEGY_PRIOR, PRIOR_PREMIUM_DIMENSION, null, null, ghName, fetchLimit, false);
+                dt, crowdSegment, DEMAND_STRATEGY_PRIOR, PRIOR_PREMIUM_DIMENSION, null, null, ghName, null, fetchLimit, false);
         List<ContentPlatformDemandVideo> stageB = demandVideoMapperExt.selectForRecommend(
-                dt, crowdSegment, DEMAND_STRATEGY_PRIOR, null, PRIOR_PREMIUM_DIMENSION, null, ghName, fetchLimit, false);
+                dt, crowdSegment, DEMAND_STRATEGY_PRIOR, null, PRIOR_PREMIUM_DIMENSION, null, ghName, null, fetchLimit, false);
 
         // 退化:该 ghName 在两阶段都无数据 → 退回渠道粒度
         if (ghName != null && stageA.isEmpty() && stageB.isEmpty()) {
             stageA = demandVideoMapperExt.selectForRecommend(
-                    dt, crowdSegment, DEMAND_STRATEGY_PRIOR, PRIOR_PREMIUM_DIMENSION, null, null, null, fetchLimit, false);
+                    dt, crowdSegment, DEMAND_STRATEGY_PRIOR, PRIOR_PREMIUM_DIMENSION, null, null, null, null, fetchLimit, false);
             stageB = demandVideoMapperExt.selectForRecommend(
-                    dt, crowdSegment, DEMAND_STRATEGY_PRIOR, null, PRIOR_PREMIUM_DIMENSION, null, null, fetchLimit, false);
+                    dt, crowdSegment, DEMAND_STRATEGY_PRIOR, null, PRIOR_PREMIUM_DIMENSION, null, null, null, fetchLimit, false);
         }
 
         Function<ContentPlatformDemandVideo, String> keyFn = r ->
@@ -808,16 +882,16 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
         int fetchLimit = Math.max(limit * 3, DEMAND_CANDIDATE_LIMIT);
 
         List<ContentPlatformDemandVideo> stageAbs = demandVideoMapperExt.selectForRecommend(
-                dt, crowdSegment, DEMAND_STRATEGY_POSTERIOR, null, null, POSTERIOR_FILTER_ABS_LIKE, ghName, fetchLimit, true);
+                dt, crowdSegment, DEMAND_STRATEGY_POSTERIOR, null, null, POSTERIOR_FILTER_ABS_LIKE, ghName, POSTERIOR_DRIVE_DIMENSION_TIME, fetchLimit, true);
         List<ContentPlatformDemandVideo> stageRel = demandVideoMapperExt.selectForRecommend(
-                dt, crowdSegment, DEMAND_STRATEGY_POSTERIOR, null, null, POSTERIOR_FILTER_REL_LIKE, ghName, fetchLimit, true);
+                dt, crowdSegment, DEMAND_STRATEGY_POSTERIOR, null, null, POSTERIOR_FILTER_REL_LIKE, ghName, POSTERIOR_DRIVE_DIMENSION_TIME, fetchLimit, true);
 
-        // 退化:该 ghName 在两阶段都无数据 → 退回渠道粒度
+        // 退化:该 ghName 在两阶段都无数据 → 退回渠道粒度(drive_dimension_time 仍严格为"昨日")
         if (ghName != null && stageAbs.isEmpty() && stageRel.isEmpty()) {
             stageAbs = demandVideoMapperExt.selectForRecommend(
-                    dt, crowdSegment, DEMAND_STRATEGY_POSTERIOR, null, null, POSTERIOR_FILTER_ABS_LIKE, null, fetchLimit, true);
+                    dt, crowdSegment, DEMAND_STRATEGY_POSTERIOR, null, null, POSTERIOR_FILTER_ABS_LIKE, null, POSTERIOR_DRIVE_DIMENSION_TIME, fetchLimit, true);
             stageRel = demandVideoMapperExt.selectForRecommend(
-                    dt, crowdSegment, DEMAND_STRATEGY_POSTERIOR, null, null, POSTERIOR_FILTER_REL_LIKE, null, fetchLimit, true);
+                    dt, crowdSegment, DEMAND_STRATEGY_POSTERIOR, null, null, POSTERIOR_FILTER_REL_LIKE, null, POSTERIOR_DRIVE_DIMENSION_TIME, fetchLimit, true);
         }
 
         Function<ContentPlatformDemandVideo, String> keyFn = r ->

+ 3 - 0
api-module/src/main/resources/mapper/contentplatform/ext/ContentPlatformDemandVideoMapperExt.xml

@@ -102,6 +102,9 @@
         <if test="channelLevel3 != null and channelLevel3 != ''">
             AND channel_level3 = #{channelLevel3}
         </if>
+        <if test="driveDimensionTime != null and driveDimensionTime != ''">
+            AND drive_dimension_time = #{driveDimensionTime}
+        </if>
         <if test="excludeSelfTitle">
             AND (title IS NULL OR demand_content_title IS NULL OR title &lt;&gt; demand_content_title)
         </if>