|
@@ -612,10 +612,12 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
|
|
|
private static final int HOT_CANDIDATE_LIMIT = 10000;
|
|
private static final int HOT_CANDIDATE_LIMIT = 10000;
|
|
|
private static final int TOP_K_PER_DEMAND = 3;
|
|
private static final int TOP_K_PER_DEMAND = 3;
|
|
|
private static final String DEMAND_STRATEGY_PRIOR = "先验需求";
|
|
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 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_ABS_LIKE = "绝对高效率%";
|
|
|
private static final String POSTERIOR_FILTER_REL_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_PRIOR = "prior";
|
|
|
private static final String SOURCE_POSTERIOR = "posterior";
|
|
private static final String SOURCE_POSTERIOR = "posterior";
|
|
|
private static final String SOURCE_HOT = "hot";
|
|
private static final String SOURCE_HOT = "hot";
|
|
@@ -648,15 +650,48 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
|
|
|
if (SOURCE_HOT.equals(source)) {
|
|
if (SOURCE_HOT.equals(source)) {
|
|
|
return getHotSourcePaged(param, user);
|
|
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) {
|
|
for (VideoContentItemVO v : list) {
|
|
|
v.setSource(source);
|
|
v.setSource(source);
|
|
|
}
|
|
}
|
|
|
return paginateCandidates(param, list);
|
|
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 真分页链路。
|
|
* 单源 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) {
|
|
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> prior = fetchPriorCandidates(param, user, DEMAND_CANDIDATE_LIMIT);
|
|
|
List<VideoContentItemVO> posterior = fetchPosteriorCandidates(param, user, DEMAND_CANDIDATE_LIMIT);
|
|
List<VideoContentItemVO> posterior = fetchPosteriorCandidates(param, user, DEMAND_CANDIDATE_LIMIT);
|
|
|
List<VideoContentItemVO> hot = fetchHotCandidates(param, user, HOT_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 : prior) v.setSource(SOURCE_PRIOR);
|
|
|
for (VideoContentItemVO v : posterior) v.setSource(SOURCE_POSTERIOR);
|
|
for (VideoContentItemVO v : posterior) v.setSource(SOURCE_POSTERIOR);
|
|
|
for (VideoContentItemVO v : hot) v.setSource(SOURCE_HOT);
|
|
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<>();
|
|
Set<Long> emittedIds = new HashSet<>();
|
|
|
List<VideoContentItemVO> merged = new ArrayList<>();
|
|
List<VideoContentItemVO> merged = new ArrayList<>();
|
|
|
|
|
|
|
@@ -722,9 +761,15 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
|
|
|
long seed = userSeed ^ LocalDate.now().toString().hashCode();
|
|
long seed = userSeed ^ LocalDate.now().toString().hashCode();
|
|
|
Random rng = new Random(seed);
|
|
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);
|
|
if (!exhausted[i]) alive.add(i);
|
|
|
}
|
|
}
|
|
|
int cur = alive.get(rng.nextInt(alive.size()));
|
|
int cur = alive.get(rng.nextInt(alive.size()));
|
|
@@ -759,6 +804,35 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
|
|
|
return result;
|
|
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。
|
|
* 先验池:A 段 dimension='传播头部' → B 段 其余 dimension。
|
|
|
* 每段按 (point_type, standard_element) 分组,组按 total_rov DESC、组内 score DESC 取前 K;段间拼接 + video_id 去重。
|
|
* 每段按 (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);
|
|
int fetchLimit = Math.max(limit * 3, DEMAND_CANDIDATE_LIMIT);
|
|
|
|
|
|
|
|
List<ContentPlatformDemandVideo> stageA = demandVideoMapperExt.selectForRecommend(
|
|
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(
|
|
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 在两阶段都无数据 → 退回渠道粒度
|
|
// 退化:该 ghName 在两阶段都无数据 → 退回渠道粒度
|
|
|
if (ghName != null && stageA.isEmpty() && stageB.isEmpty()) {
|
|
if (ghName != null && stageA.isEmpty() && stageB.isEmpty()) {
|
|
|
stageA = demandVideoMapperExt.selectForRecommend(
|
|
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(
|
|
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 ->
|
|
Function<ContentPlatformDemandVideo, String> keyFn = r ->
|
|
@@ -808,16 +882,16 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
|
|
|
int fetchLimit = Math.max(limit * 3, DEMAND_CANDIDATE_LIMIT);
|
|
int fetchLimit = Math.max(limit * 3, DEMAND_CANDIDATE_LIMIT);
|
|
|
|
|
|
|
|
List<ContentPlatformDemandVideo> stageAbs = demandVideoMapperExt.selectForRecommend(
|
|
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(
|
|
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()) {
|
|
if (ghName != null && stageAbs.isEmpty() && stageRel.isEmpty()) {
|
|
|
stageAbs = demandVideoMapperExt.selectForRecommend(
|
|
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(
|
|
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 ->
|
|
Function<ContentPlatformDemandVideo, String> keyFn = r ->
|