|
|
@@ -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();
|