Parcourir la source

interleaveMultiPools 多池顺序轮转

wangyunpeng il y a 1 jour
Parent
commit
0bc1ed7999

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

@@ -790,9 +790,7 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
                     for (VideoContentItemVO v : pool) v.setSource(VideoContentSource.PRIOR.getValue());
                 }
 
-                long userSeed = user.getId() == null ? 0L : user.getId();
-                long seed = System.nanoTime() ^ userSeed;
-                list = interleaveMultiPools(pools, new Random(seed), 1);
+                list = interleaveMultiPools(pools);
             } finally {
                 executor.shutdown();
             }
@@ -806,54 +804,11 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
     }
 
     /**
-     * 3 池每位独立等概率抽样(公众号入口用):
-     * - 池: [scene, prior(传播头部), growth(增长头部)]
-     * - 每个输出位置在「未耗尽池」中等概率抽 1,从该池头部取下一条
-     * - seed = nanoTime ^ userId:每次接口请求都换 seed,第一条来自哪一路每次都不同
-     * - 跨池 video_id / 标题去重;翻页 P1/P2 不保证序列一致(刷新即换排)
+     * 通用 N 池轮转穿插(确定性,无随机):
+     * - 池按传入顺序轮转,每池取 1 条,跳过已耗尽的池继续下一轮
+     * - 跨池 video_id / 标题去重;某池耗尽后自动从轮转中移除
      */
-    private List<VideoContentItemVO> interleavePriorPoolsRandom(List<VideoContentItemVO> scene,
-                                                                List<VideoContentItemVO> prior,
-                                                                List<VideoContentItemVO> growth,
-                                                                ContentPlatformAccount user) {
-        long userSeed = (user == null || user.getId() == null) ? 0L : user.getId();
-        long seed = System.nanoTime() ^ userSeed;
-        return interleaveMultiPools(Arrays.asList(scene, prior, growth), new Random(seed), 1);
-    }
-
-    /**
-     * 企微入口用:priorScene 与 prior 池严格 1:1 交替输出(无随机):
-     * - 起始池固定 scene,交替 1:1 各取 1 条
-     * - 跨池 video_id / 标题去重;一侧用完后,剩余按原顺序追加输出,不丢数据
-     */
-    private List<VideoContentItemVO> interleavePriorWithScene(List<VideoContentItemVO> scene,
-                                                              List<VideoContentItemVO> prior) {
-        Set<Long> seenIds = new HashSet<>();
-        Set<String> seenTitles = 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 (tryEmit(v, seenIds, seenTitles, out)) break;
-            }
-            while (pi < prior.size()) {
-                VideoContentItemVO v = prior.get(pi++);
-                if (tryEmit(v, seenIds, seenTitles, out)) break;
-            }
-        }
-        return out;
-    }
-
-    /**
-     * 通用 N 池随机穿插:
-     * - maxBlockSize=1 → 每位独立等概率从所有未耗尽池抽 1(允许连续同源)
-     * - maxBlockSize=K(>=2) → 块大小 1~K 随机,块间切「其他未耗尽池」(避免连续同源)
-     * - 跨池 video_id / 标题去重;某池跳过去重后耗尽即标记 exhausted
-     */
-    private List<VideoContentItemVO> interleaveMultiPools(List<List<VideoContentItemVO>> pools,
-                                                          Random rng,
-                                                          int maxBlockSize) {
+    private List<VideoContentItemVO> interleaveMultiPools(List<List<VideoContentItemVO>> pools) {
         int n = pools.size();
         int[] pointers = new int[n];
         boolean[] exhausted = new boolean[n];
@@ -864,36 +819,24 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
         Set<String> seenTitles = new HashSet<>();
         List<VideoContentItemVO> out = new ArrayList<>();
 
-        int current = -1;
+        int cur = 0;
         while (true) {
-            List<Integer> alive = new ArrayList<>(n);
-            for (int i = 0; i < n; i++) if (!exhausted[i]) alive.add(i);
-            if (alive.isEmpty()) break;
-
-            if (maxBlockSize <= 1) {
-                // K=1: 每位都从所有未耗尽池等概率抽,允许连续同源
-                current = alive.get(rng.nextInt(alive.size()));
-            } else if (current < 0 || exhausted[current]) {
-                // K>=2 首次或当前池耗尽: 从所有未耗尽池随机
-                current = alive.get(rng.nextInt(alive.size()));
-            } else if (alive.size() > 1) {
-                // K>=2 块切换: 从「其他未耗尽池」随机抽 1
-                List<Integer> others = new ArrayList<>(alive.size() - 1);
-                for (int i : alive) if (i != current) others.add(i);
-                current = others.get(rng.nextInt(others.size()));
+            // 跳过已耗尽的池
+            int started = cur;
+            while (exhausted[cur]) {
+                cur = (cur + 1) % n;
+                if (cur == started) return out; // 全部耗尽
             }
-            // alive.size()==1 时 current 维持(只剩这一池,直到耗尽)
-
-            int blockSize = maxBlockSize <= 1 ? 1 : 1 + rng.nextInt(maxBlockSize);
-            int emitted = 0;
-            List<VideoContentItemVO> pool = pools.get(current);
-            while (emitted < blockSize && pointers[current] < pool.size()) {
-                VideoContentItemVO v = pool.get(pointers[current]++);
-                if (tryEmit(v, seenIds, seenTitles, out)) emitted++;
+
+            List<VideoContentItemVO> pool = pools.get(cur);
+            while (pointers[cur] < pool.size()) {
+                VideoContentItemVO v = pool.get(pointers[cur]++);
+                if (tryEmit(v, seenIds, seenTitles, out)) break;
             }
-            if (pointers[current] >= pool.size()) exhausted[current] = true;
+            if (pointers[cur] >= pool.size()) exhausted[cur] = true;
+
+            cur = (cur + 1) % n;
         }
-        return out;
     }
 
     private boolean tryEmit(VideoContentItemVO v, Set<Long> seenIds, Set<String> seenTitles, List<VideoContentItemVO> out) {
@@ -1000,65 +943,13 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
             for (VideoContentItemVO v : pools.get(priorCount)) v.setSource(VideoContentSource.POSTERIOR.getValue());
             for (VideoContentItemVO v : pools.get(priorCount + 1)) v.setSource(VideoContentSource.HOT.getValue());
 
-            int N = pools.size();
-            int[] pointers = new int[N];
-            boolean[] exhausted = new boolean[N];
-            Set<Long> emittedIds = new HashSet<>();
-            Set<String> emittedTitles = new HashSet<>();
-            List<VideoContentItemVO> merged = new ArrayList<>();
-
-            long userSeed = user.getId() == null ? 0L : user.getId();
-            long seed = userSeed ^ LocalDate.now().toString().hashCode();
-            Random rng = new Random(seed);
-
-            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()));
-
-                List<VideoContentItemVO> pool = pools.get(cur);
-                while (pointers[cur] < pool.size() && shouldSkipForDedup(pool.get(pointers[cur]), emittedIds, emittedTitles)) {
-                    pointers[cur]++;
-                }
-                if (pointers[cur] < pool.size()) {
-                    VideoContentItemVO item = pool.get(pointers[cur]++);
-                    emittedIds.add(item.getVideoId());
-                    String nt = TitleNormalizer.normalize(item.getTitle());
-                    if (!nt.isEmpty()) emittedTitles.add(nt);
-                    merged.add(item);
-                } else {
-                    exhausted[cur] = true;
-                }
-            }
+            List<VideoContentItemVO> merged = interleaveMultiPools(pools);
             return paginateCandidates(param, merged);
         } finally {
             executor.shutdown();
         }
     }
 
-    /**
-     * 穿插去重判断:同 video_id 已出过 → 跳;同标题(归一化后)已出过 → 跳。
-     * 标题归一化用 TitleNormalizer(去 emoji/空白/全半角),应对运营把同段内容重复上传成多个 video_id 的情况。
-     */
-    private boolean shouldSkipForDedup(VideoContentItemVO item, Set<Long> emittedIds, Set<String> emittedTitles) {
-        if (item.getVideoId() != null && emittedIds.contains(item.getVideoId())) {
-            return true;
-        }
-        String nt = TitleNormalizer.normalize(item.getTitle());
-        return !nt.isEmpty() && emittedTitles.contains(nt);
-    }
-
     private Page<VideoContentItemVO> paginateCandidates(VideoContentListParam param, List<VideoContentItemVO> all) {
         int pageSize = param.getPageSize();
         int pageNum = param.getPageNum();