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

场景池改按视频 rov 排序

先验需求-场景池排序逻辑改造:
- 同 video_id 去重保留 total_rov 最大的代表行
- 过滤 rov 为 null 或 <=0 的视频
- 按 rov DESC 排序(次级 total_rov DESC)

避免按需求维度排序导致同 dimension 视频扎堆。
刘立冬 16 часов назад
Родитель
Сommit
1d2c32f975

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

@@ -805,9 +805,12 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
     }
     }
 
 
     /**
     /**
-     * 先验需求-场景池: demand_strategy='先验需求-场景',直接按 total_rov DESC, score DESC 取 top N,不分组
+     * 先验需求-场景池: demand_strategy='先验需求-场景'。
      * 退化策略: ghName 非空且查不到数据 → 退回渠道粒度(不限 channel_level3)。
      * 退化策略: ghName 非空且查不到数据 → 退回渠道粒度(不限 channel_level3)。
-     * 行内不过滤 rov<=0(场景型命中数据 rov 通常都有值,且这类数据稀缺,不再二次过滤)。
+     * 后处理:
+     *   1. 同 video_id 仅保留 total_rov 最大的代表行(利用 SQL 已按 total_rov DESC, score DESC 排好,首次即最大)
+     *   2. 过滤 rov 为 null 或 <=0(视频近 7 日无表现)
+     *   3. 输出顺序按 rov DESC,相同 rov 按 total_rov DESC 兜底
      */
      */
     private List<VideoContentItemVO> fetchPriorSceneCandidates(VideoContentListParam param, ContentPlatformAccount user, int limit) {
     private List<VideoContentItemVO> fetchPriorSceneCandidates(VideoContentListParam param, ContentPlatformAccount user, int limit) {
         String dt = demandVideoMapperExt.getMaxDt();
         String dt = demandVideoMapperExt.getMaxDt();
@@ -823,13 +826,28 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
             rows = demandVideoMapperExt.selectForRecommend(
             rows = demandVideoMapperExt.selectForRecommend(
                     dt, crowdSegment, DEMAND_STRATEGY_PRIOR_SCENE, null, null, null, null, null, limit, false);
                     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<>();
+        // 1. 同 video_id 取 total_rov 最大的代表行(SQL 已排序,putIfAbsent 保留首次)
+        LinkedHashMap<Long, ContentPlatformDemandVideo> bestPerVideo = new LinkedHashMap<>();
         for (ContentPlatformDemandVideo r : rows) {
         for (ContentPlatformDemandVideo r : rows) {
             if (r.getVideoId() == null) continue;
             if (r.getVideoId() == null) continue;
-            if (!seen.add(r.getVideoId())) continue;
+            bestPerVideo.putIfAbsent(r.getVideoId(), r);
+        }
+        // 2. 过滤 rov<=0/null
+        List<ContentPlatformDemandVideo> filtered = new ArrayList<>(bestPerVideo.size());
+        for (ContentPlatformDemandVideo r : bestPerVideo.values()) {
+            if (r.getRov() == null || r.getRov() <= 0) continue;
             filtered.add(r);
             filtered.add(r);
         }
         }
+        // 3. 按 rov DESC 排序,次级 total_rov DESC
+        filtered.sort((a, b) -> {
+            int c = Double.compare(
+                    b.getRov() == null ? 0d : b.getRov(),
+                    a.getRov() == null ? 0d : a.getRov());
+            if (c != 0) return c;
+            return Double.compare(
+                    b.getTotalRov() == null ? 0d : b.getTotalRov(),
+                    a.getTotalRov() == null ? 0d : a.getTotalRov());
+        });
         return buildDemandVideoContentItemVOList(filtered);
         return buildDemandVideoContentItemVOList(filtered);
     }
     }
 
 

+ 11 - 9
docs/recommend-ordering.md

@@ -45,7 +45,7 @@ source 空(默认)  ──────►  四路随机穿插 (getInterleave
 
 
 ### 3.1 `fetchPriorSceneCandidates`(场景池)
 ### 3.1 `fetchPriorSceneCandidates`(场景池)
 
 
-**目的:用户所属 channel 在"场景"维度命中的先验。**
+**目的:用户所属 channel 在"场景"维度命中的先验,按视频近 7 日表现(rov)排序。**
 
 
 ```sql
 ```sql
 SELECT ... FROM content_platform_demand_video
 SELECT ... FROM content_platform_demand_video
@@ -59,12 +59,14 @@ LIMIT 10000
 
 
 退化:如果 `ghName` 非空但查 0 条,**去掉 `ghName` 再查一次**(拿全渠道兜底)。
 退化:如果 `ghName` 非空但查 0 条,**去掉 `ghName` 再查一次**(拿全渠道兜底)。
 
 
-后处理:
-1. `videoId` 为 null 的丢弃
-2. 同 `videoId` 仅保留首次
-3. **不做分组、不做 rov<=0 过滤**(场景型本身稀缺)
+后处理(顺序)
+1. 同 `videoId` 去重,保留首次出现(SQL 已按 `total_rov DESC, score DESC` 排序 → 首次 = 该视频的"最强代表需求"行)
+2. **过滤** `rov` 为 null 或 ≤0 的视频
+3. 重排序:`rov DESC` 主键,`total_rov DESC` 次级 tiebreaker
 
 
-输出顺序 = SQL 返回顺序 = `total_rov DESC, score DESC`。
+输出顺序:**视频按 rov(近 7 日表现)DESC**;同 rov 时按代表需求的 total_rov DESC。
+
+> 设计动机:场景需求里同一视频会对应多个特征点,但前端只下发一条;用 total_rov 选"最强代表需求",再以视频自身的 rov 决定整体排序,避免按需求维度排序导致同 dimension/标准要素的视频在列表里扎堆。
 
 
 ---
 ---
 
 
@@ -168,7 +170,7 @@ hot        → 标 source='hot'
 
 
 ### 排序稳定性
 ### 排序稳定性
 - 同一用户同一天,所有分页之间顺序一致
 - 同一用户同一天,所有分页之间顺序一致
-- `priorScene` / `prior` / `posterior` 内部相对顺序保留(场景 → 总 ROV → score),随机只影响"哪一池先出"
+- `priorScene` / `prior` / `posterior` 内部相对顺序保留(场景按视频 rov;先验/后验按组 total_rov + 组内 score),随机只影响"哪一池先出"
 
 
 ---
 ---
 
 
@@ -225,8 +227,8 @@ list = fetchPosteriorCandidates(...)   // 顺序 = 绝对高效率段 → 相对
    ┌───────────────────────┬──┴──┬─────────────────────────────┐
    ┌───────────────────────┬──┴──┬─────────────────────────────┐
    ▼                       ▼     ▼                             ▼
    ▼                       ▼     ▼                             ▼
 priorScene(10000)    prior(10000)  posterior(10000)         hot(10000)
 priorScene(10000)    prior(10000)  posterior(10000)         hot(10000)
-  total_rov DESC,    A: 传播头部     A: 绝对高效率 + 昨日       SQL 默认
-  score DESC          B: 其他维度    B: 相对裂变率 + 昨日       (sort 决定)
+  视频维度 rov DESC   传播头部       A: 绝对高效率 + 昨日       SQL 默认
+  (代表需求 total_rov 选最大)        B: 相对裂变率 + 昨日       (sort 决定)
                      组(point_type, standard_element) top3
                      组(point_type, standard_element) top3
                      组(demand_content_id) top3
                      组(demand_content_id) top3