|
|
@@ -694,10 +694,11 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
|
|
|
}
|
|
|
List<VideoContentItemVO> list;
|
|
|
if (SOURCE_PRIOR.equals(source)) {
|
|
|
- // 粉丝喜欢 = 人群需求-场景 与 人群需求 严格 1:1 穿插,场景先出,prior 用完顺位补齐
|
|
|
+ // 粉丝喜欢 = priorScene(场景已看视频) 与 prior(人群需求·票圈推荐库) 池间交替,块大小 1~2 随机,起始池由 seed 决定;
|
|
|
+ // K=2 保证两池 top-2 必在前 4 条,池间交替避免连续同源,seed=userId^date 同用户同日稳定
|
|
|
List<VideoContentItemVO> scene = fetchPriorSceneCandidates(param, user, DEMAND_CANDIDATE_LIMIT);
|
|
|
List<VideoContentItemVO> prior = fetchPriorCandidates(param, user, DEMAND_CANDIDATE_LIMIT);
|
|
|
- list = interleavePriorWithScene(scene, prior);
|
|
|
+ list = interleavePriorWithScene(scene, prior, user);
|
|
|
} else {
|
|
|
list = fetchPosteriorCandidates(param, user, DEMAND_CANDIDATE_LIMIT);
|
|
|
}
|
|
|
@@ -708,23 +709,45 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * priorScene 与 prior 严格 1:1 穿插 + 跨池 video_id / 标题 去重(priorScene 优先到达)。
|
|
|
- * 一侧用完后,另一侧剩余按原顺序追加。
|
|
|
+ * priorScene 与 prior 池间交替混合输出:
|
|
|
+ * - 块大小 1~2 随机(K=2),池间交替;起始池由 seed 决定
|
|
|
+ * - seed = userId XOR LocalDate.toString().hashCode():同一用户同一天刷新顺序稳定,跨用户/跨日不同
|
|
|
+ * - K=2 保证两池 top-2 必落在前 4 条(只是顺序按 seed 微调,不再机械 1:1)
|
|
|
+ * - 跨池 video_id / 标题去重;一侧用完后,剩余按原顺序追加输出,不丢数据
|
|
|
*/
|
|
|
- private List<VideoContentItemVO> interleavePriorWithScene(List<VideoContentItemVO> scene, List<VideoContentItemVO> prior) {
|
|
|
+ private List<VideoContentItemVO> interleavePriorWithScene(List<VideoContentItemVO> scene,
|
|
|
+ List<VideoContentItemVO> prior,
|
|
|
+ ContentPlatformAccount user) {
|
|
|
Set<Long> seenIds = new HashSet<>();
|
|
|
Set<String> seenTitles = new HashSet<>();
|
|
|
List<VideoContentItemVO> out = new ArrayList<>();
|
|
|
int si = 0, pi = 0;
|
|
|
+
|
|
|
+ long userSeed = (user == null || user.getId() == null) ? 0L : user.getId();
|
|
|
+ long seed = userSeed ^ LocalDate.now().toString().hashCode();
|
|
|
+ Random rng = new Random(seed);
|
|
|
+
|
|
|
+ boolean fromScene = rng.nextBoolean();
|
|
|
+
|
|
|
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;
|
|
|
+ // 当前选中的池已空 → 强制切到另一池
|
|
|
+ if (fromScene && si >= scene.size()) fromScene = false;
|
|
|
+ else if (!fromScene && pi >= prior.size()) fromScene = true;
|
|
|
+
|
|
|
+ int blockSize = 1 + rng.nextInt(2); // 1 or 2
|
|
|
+ int emitted = 0;
|
|
|
+ if (fromScene) {
|
|
|
+ while (si < scene.size() && emitted < blockSize) {
|
|
|
+ VideoContentItemVO v = scene.get(si++);
|
|
|
+ if (tryEmit(v, seenIds, seenTitles, out)) emitted++;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ while (pi < prior.size() && emitted < blockSize) {
|
|
|
+ VideoContentItemVO v = prior.get(pi++);
|
|
|
+ if (tryEmit(v, seenIds, seenTitles, out)) emitted++;
|
|
|
+ }
|
|
|
}
|
|
|
+ fromScene = !fromScene;
|
|
|
}
|
|
|
return out;
|
|
|
}
|