|
@@ -149,6 +149,22 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
|
|
|
@Value("${small_page_url}")
|
|
@Value("${small_page_url}")
|
|
|
private String GET_SMALL_PAGE_URL;
|
|
private String GET_SMALL_PAGE_URL;
|
|
|
|
|
|
|
|
|
|
+ /** prior/posterior 池视频近 7 日 rov 下限(质量过滤) */
|
|
|
|
|
+ @Value("${demand.min.rov:0.03}")
|
|
|
|
|
+ private double demandMinRov;
|
|
|
|
|
+
|
|
|
|
|
+ /** priorScene 池(场景已看视频)的 sceneSumRov 下限(质量过滤) */
|
|
|
|
|
+ @Value("${demand.prior.scene.min.sum.rov:0.03}")
|
|
|
|
|
+ private double priorSceneMinSumRov;
|
|
|
|
|
+
|
|
|
|
|
+ /** prior 池按(point_type, standard_element)分组后,保留 total_rov 排名 top K 的比例 */
|
|
|
|
|
+ @Value("${demand.prior.group.keep.ratio:0.5}")
|
|
|
|
|
+ private double priorGroupKeepRatio;
|
|
|
|
|
+
|
|
|
|
|
+ /** posterior 池按 demand_content_id 分组后,保留 total_rov 排名 top K 的比例 */
|
|
|
|
|
+ @Value("${demand.posterior.group.keep.ratio:0.5}")
|
|
|
|
|
+ private double posteriorGroupKeepRatio;
|
|
|
|
|
+
|
|
|
@Override
|
|
@Override
|
|
|
public Page<GzhPlanItemVO> gzhPlanList(GzhPlanListParam param) {
|
|
public Page<GzhPlanItemVO> gzhPlanList(GzhPlanListParam param) {
|
|
|
ContentPlatformAccount loginAccount = LoginUserContext.getUser();
|
|
ContentPlatformAccount loginAccount = LoginUserContext.getUser();
|
|
@@ -616,80 +632,81 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
|
|
|
private static final int DEMAND_CANDIDATE_LIMIT = 10000;
|
|
private static final int DEMAND_CANDIDATE_LIMIT = 10000;
|
|
|
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_POSTERIOR = "优质相似";
|
|
|
|
|
- private static final String PRIOR_PREMIUM_DIMENSION = "传播的头部";
|
|
|
|
|
- /** 第三路池:dimension='增长的头部',与 PRIOR_PREMIUM_DIMENSION 语义不同(增长强度 vs 传播强度)。 */
|
|
|
|
|
- private static final String GROWTH_PREMIUM_DIMENSION = "增长的头部";
|
|
|
|
|
- /** match_method 取值,priorScene 池识别用(0519+ 起替代旧 demand_strategy='人群需求-场景') */
|
|
|
|
|
- private static final String MATCH_METHOD_PRIOR_SCENE = "场景已看视频";
|
|
|
|
|
- /** match_method 取值,prior / posterior 池识别用 */
|
|
|
|
|
- private static final String MATCH_METHOD_PRIOR = "视频库_解构特征_向量相似匹配";
|
|
|
|
|
- /** prior/posterior 池视频近 7 日 rov 下限(质量过滤):统一与 priorScene sceneSumRov 一致,0.03。 */
|
|
|
|
|
- private static final double DEMAND_MIN_ROV = 0.03;
|
|
|
|
|
- /** priorScene 池(场景已看视频)的 sceneSumRov 下限(质量过滤):与 rov 下限同口径 0.03。 */
|
|
|
|
|
- private static final double PRIOR_SCENE_MIN_SUM_ROV = 0.03;
|
|
|
|
|
- /** channel_name 映射:企微/小程序 type 直推,公众号入口按 ghName 反查 demand 表(见 resolveChannelName)。 */
|
|
|
|
|
- private static final String CHANNEL_NAME_QW = "群/企微合作-稳定";
|
|
|
|
|
- private static final String CHANNEL_NAME_XCX = "小程序投流-稳定";
|
|
|
|
|
- private static final String CHANNEL_NAME_GZH_TOULIU = "公众号投流-稳定";
|
|
|
|
|
- private static final String CHANNEL_NAME_GZH_JIZHUAN = "公众号合作-即转-稳定";
|
|
|
|
|
- private static final String CHANNEL_NAME_GZH_DAILY = "公众号合作-Daily-自选";
|
|
|
|
|
- /** 搜索入口仅对这两个渠道放开:小程序投流(按人群包)+ 公众号投流-稳定(按公众号)。 */
|
|
|
|
|
- private static final Set<String> CHANNELS_ALLOW_SEARCH = new HashSet<>(Arrays.asList(
|
|
|
|
|
- CHANNEL_NAME_XCX, CHANNEL_NAME_GZH_TOULIU));
|
|
|
|
|
- /** 搜索入口仅对 INTERNAL(2 自营)/ AGENT(3 代理) 类型账号放开。 */
|
|
|
|
|
- private static final Set<Integer> USER_TYPES_ALLOW_SEARCH = new HashSet<>(Arrays.asList(2, 3));
|
|
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
- * 合作类渠道:demand.crowd_segment 语义=合作方代码(yy/szhx/cdjh/...),与登录账号 ContentPlatformAccount.channel 同义,可作为过滤条件。
|
|
|
|
|
- * 其余渠道(投流-稳定/服务号投流):demand.crowd_segment 语义=人群分组标签(R50*xx/回流xx),与登录账号合作方无关,绝不可用 user.channel 过滤。
|
|
|
|
|
|
|
+ * 人群需求池配置:{dimension, demandType, matchMethod, isScene}
|
|
|
*/
|
|
*/
|
|
|
- private static final Set<String> CHANNELS_USE_PARTNER_AS_CROWD = new HashSet<>(Arrays.asList(
|
|
|
|
|
- CHANNEL_NAME_QW, CHANNEL_NAME_GZH_JIZHUAN, CHANNEL_NAME_GZH_DAILY));
|
|
|
|
|
- private static final double PRIOR_GROUP_KEEP_RATIO = 0.5;
|
|
|
|
|
- /** posterior 按 demand_content_id 分组后保留 total_rov 排名前 50% 的需求组,
|
|
|
|
|
- * 砍掉群体表现弱的需求,避免低 total_rov 的 demand 带回来的相似变体稀释结果。 */
|
|
|
|
|
- private static final double POSTERIOR_GROUP_KEEP_RATIO = 0.5;
|
|
|
|
|
- private static final String SOURCE_PRIOR = "prior";
|
|
|
|
|
- private static final String SOURCE_POSTERIOR = "posterior";
|
|
|
|
|
- private static final String SOURCE_HOT = "hot";
|
|
|
|
|
|
|
+ private static final List<PriorPoolConfig> PRIOR_POOL_CONFIGS = buildPriorPoolConfigs();
|
|
|
|
|
+
|
|
|
|
|
+ private static List<PriorPoolConfig> buildPriorPoolConfigs() {
|
|
|
|
|
+ return Arrays.asList(
|
|
|
|
|
+ // 策略1: 人群需求*场景已看
|
|
|
|
|
+ new PriorPoolConfig(PriorDimensionEnum.PREMIUM, DemandTypeEnum.STANDARD, DemandMatchMethodEnum.SCENE, true),
|
|
|
|
|
+ // 策略2: 人群需求*特征点*向量匹配
|
|
|
|
|
+ new PriorPoolConfig(PriorDimensionEnum.PREMIUM, DemandTypeEnum.STANDARD, DemandMatchMethodEnum.VECTOR_SIMILARITY, false),
|
|
|
|
|
+ new PriorPoolConfig(PriorDimensionEnum.GROWTH, DemandTypeEnum.STANDARD, DemandMatchMethodEnum.VECTOR_SIMILARITY, false),
|
|
|
|
|
+ new PriorPoolConfig(PriorDimensionEnum.DISTRIBUTION, DemandTypeEnum.STANDARD, DemandMatchMethodEnum.VECTOR_SIMILARITY, false),
|
|
|
|
|
+ // 策略3: 人群需求*特征点泛化*向量匹配
|
|
|
|
|
+ new PriorPoolConfig(PriorDimensionEnum.PREMIUM, DemandTypeEnum.GENERALIZED, DemandMatchMethodEnum.VECTOR_SIMILARITY, false),
|
|
|
|
|
+ new PriorPoolConfig(PriorDimensionEnum.GROWTH, DemandTypeEnum.GENERALIZED, DemandMatchMethodEnum.VECTOR_SIMILARITY, false),
|
|
|
|
|
+ new PriorPoolConfig(PriorDimensionEnum.DISTRIBUTION, DemandTypeEnum.GENERALIZED, DemandMatchMethodEnum.VECTOR_SIMILARITY, false),
|
|
|
|
|
+ // 策略4: 人群需求*特征点泛化*精准匹配
|
|
|
|
|
+ new PriorPoolConfig(PriorDimensionEnum.PREMIUM, DemandTypeEnum.GENERALIZED, DemandMatchMethodEnum.PRECISION, false),
|
|
|
|
|
+ new PriorPoolConfig(PriorDimensionEnum.GROWTH, DemandTypeEnum.GENERALIZED, DemandMatchMethodEnum.PRECISION, false),
|
|
|
|
|
+ new PriorPoolConfig(PriorDimensionEnum.DISTRIBUTION, DemandTypeEnum.GENERALIZED, DemandMatchMethodEnum.PRECISION, false)
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 人群需求单池配置
|
|
|
|
|
+ */
|
|
|
|
|
+ private static class PriorPoolConfig {
|
|
|
|
|
+ final PriorDimensionEnum dimension;
|
|
|
|
|
+ final DemandTypeEnum demandType;
|
|
|
|
|
+ final DemandMatchMethodEnum matchMethod;
|
|
|
|
|
+ final boolean scene;
|
|
|
|
|
+
|
|
|
|
|
+ PriorPoolConfig(PriorDimensionEnum dimension, DemandTypeEnum demandType, DemandMatchMethodEnum matchMethod, boolean scene) {
|
|
|
|
|
+ this.dimension = dimension;
|
|
|
|
|
+ this.demandType = demandType;
|
|
|
|
|
+ this.matchMethod = matchMethod;
|
|
|
|
|
+ this.scene = scene;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* 推导 channel_name(人群_渠道) 作为 demand 池强过滤。
|
|
* 推导 channel_name(人群_渠道) 作为 demand 池强过滤。
|
|
|
* 信号优先级:
|
|
* 信号优先级:
|
|
|
- * 1. 企微入口(type=2/3) → 群/企微合作-稳定
|
|
|
|
|
- * 2. 小程序投流(type=5) → 小程序投流-稳定
|
|
|
|
|
- * 3. 公众号入口(type=0/1/4)或仅传 ghName:按 ghName 反查 demand 表 channel_level3 → channel_name
|
|
|
|
|
- * ─ 业务约定一个公众号只归属一个 channel(合作-即转 / 合作-Daily-自选 / 投流-稳定 三选一),
|
|
|
|
|
- * 反查失败(新账号或上游当天没产)返回 null,不限 channel_name(保留兜底行为)。
|
|
|
|
|
- * 4. 否则 null,不限 channel_name。
|
|
|
|
|
- * 反查每次实时查表(已规避索引/缓存改造),首次请求 30~660ms,后续受 InnoDB buffer pool 缓存。
|
|
|
|
|
|
|
+ * 1. 企微入口(type=2/3) → QW
|
|
|
|
|
+ * 2. 小程序投流(type=5) → XCX
|
|
|
|
|
+ * 3. 公众号入口(type=0/1/4)或仅传 ghName:按 ghName 反查 demand 表 channel_level3 → channel_name
|
|
|
|
|
+ * ─ 业务约定一个公众号只归属一个 channel(合作-即转 / 合作-Daily-自选 / 投流-稳定 三选一),
|
|
|
|
|
+ * 反查失败(新账号或上游当天没产)返回 null,不限 channel_name(保留兜底行为)。
|
|
|
|
|
+ * 4. 否则 null,不限 channel_name。
|
|
|
*/
|
|
*/
|
|
|
- private String resolveChannelName(VideoContentListParam param) {
|
|
|
|
|
- Integer type = param.getType();
|
|
|
|
|
- if (type != null) {
|
|
|
|
|
- if (type == 2 || type == 3) return CHANNEL_NAME_QW;
|
|
|
|
|
- if (type == 5) return CHANNEL_NAME_XCX;
|
|
|
|
|
|
|
+ private DemandChannelEnum resolveChannelName(VideoContentListParam param) {
|
|
|
|
|
+ IndustryTypeEnum type = param.getIndustryType();
|
|
|
|
|
+ if (type != null && type != IndustryTypeEnum.OTHER) {
|
|
|
|
|
+ if (type == IndustryTypeEnum.QW_GROUP || type == IndustryTypeEnum.QW_AUTO_REPLY) return DemandChannelEnum.QW;
|
|
|
|
|
+ if (type == IndustryTypeEnum.XCX) return DemandChannelEnum.XCX;
|
|
|
}
|
|
}
|
|
|
String ghName = param.getGhName();
|
|
String ghName = param.getGhName();
|
|
|
if (StringUtils.hasText(ghName)) {
|
|
if (StringUtils.hasText(ghName)) {
|
|
|
String dt = demandVideoMapperExt.getMaxDt(null);
|
|
String dt = demandVideoMapperExt.getMaxDt(null);
|
|
|
if (StringUtils.hasText(dt)) {
|
|
if (StringUtils.hasText(dt)) {
|
|
|
- return demandVideoMapperExt.selectChannelNameByGh(dt, ghName);
|
|
|
|
|
|
|
+ return DemandChannelEnum.fromValue(demandVideoMapperExt.selectChannelNameByGh(dt, ghName));
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
return null;
|
|
return null;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 仅对"合作类"渠道(企微/即转/Daily-自选)用 user.channel 作为 demand.crowd_segment 过滤;
|
|
|
|
|
|
|
+ * 仅对"合作类"渠道用 user.channel 作为 demand.crowd_segment 过滤;
|
|
|
* 投流/服务号类渠道一律 null(crowd_segment 是人群分组标签,与合作方无关)。
|
|
* 投流/服务号类渠道一律 null(crowd_segment 是人群分组标签,与合作方无关)。
|
|
|
- * 解决:0526 之前 user.channel 被无条件喂给所有渠道,投流类零命中后触发跨渠道退化,
|
|
|
|
|
- * 退化把 ghName 一并丢掉,导致公众号入口结果串号到其他公众号。
|
|
|
|
|
*/
|
|
*/
|
|
|
- private String resolveCrowdSegment(String channelName, ContentPlatformAccount user) {
|
|
|
|
|
- if (user == null) return null;
|
|
|
|
|
- if (channelName != null && CHANNELS_USE_PARTNER_AS_CROWD.contains(channelName)) {
|
|
|
|
|
|
|
+ private String resolveCrowdSegment(DemandChannelEnum channel, ContentPlatformAccount user) {
|
|
|
|
|
+ if (user == null || channel == null) return null;
|
|
|
|
|
+ if (channel.isPartner()) {
|
|
|
return user.getChannel();
|
|
return user.getChannel();
|
|
|
}
|
|
}
|
|
|
return null;
|
|
return null;
|
|
@@ -697,12 +714,11 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* 推荐池剔除的 category 列表(身份相关)。
|
|
* 推荐池剔除的 category 列表(身份相关)。
|
|
|
- * - 渠道「公众号合作-Daily-自选」:所有身份返回空(白名单语义保留,等价于不做 NOT IN)
|
|
|
|
|
- * - 其他渠道:内部(2)/代理(3) 仅过滤「节日祝福」;合作方(1)/其他 过滤「早中晚好」+「节日祝福」
|
|
|
|
|
- * 仅作用于推荐池(selectForRecommend);搜索路径维持现状不做 category 过滤。
|
|
|
|
|
|
|
+ * - GZH_DAILY:所有身份返回空(白名单语义保留)
|
|
|
|
|
+ * - 其他:内部(2)/代理(3) 仅过滤「节日祝福」;合作方(1)/其他 过滤「早中晚好」+「节日祝福」
|
|
|
*/
|
|
*/
|
|
|
- private List<String> resolveExcludeCategories(String channelName, ContentPlatformAccount user) {
|
|
|
|
|
- if (CHANNEL_NAME_GZH_DAILY.equals(channelName)) return Collections.emptyList();
|
|
|
|
|
|
|
+ private List<String> resolveExcludeCategories(DemandChannelEnum channel, ContentPlatformAccount user) {
|
|
|
|
|
+ if (DemandChannelEnum.GZH_DAILY == channel) return Collections.emptyList();
|
|
|
Integer type = user == null ? null : user.getType();
|
|
Integer type = user == null ? null : user.getType();
|
|
|
boolean privileged = Objects.equals(type, ContentPlatformAccountTypeEnum.INTERNAL.getVal())
|
|
boolean privileged = Objects.equals(type, ContentPlatformAccountTypeEnum.INTERNAL.getVal())
|
|
|
|| Objects.equals(type, ContentPlatformAccountTypeEnum.AGENT.getVal());
|
|
|| Objects.equals(type, ContentPlatformAccountTypeEnum.AGENT.getVal());
|
|
@@ -713,9 +729,9 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* 曝光下发过滤的纯逻辑(可单测,全渠道统一口径):
|
|
* 曝光下发过滤的纯逻辑(可单测,全渠道统一口径):
|
|
|
- * - minExposurePv 为 null 或 <=0:关闭过滤,全放行;
|
|
|
|
|
- * - match_exposure_pv 为 NULL(无曝光数据,如「场景已看视频」池):放行,不受影响;
|
|
|
|
|
- * - 有曝光数据:要求 > minExposurePv 才下发。
|
|
|
|
|
|
|
+ * - minExposurePv 为 null 或 <=0:关闭过滤,全放行;
|
|
|
|
|
+ * - match_exposure_pv 为 NULL(无曝光数据,如「场景已看视频」池):放行,不受影响;
|
|
|
|
|
+ * - 有曝光数据:要求 > minExposurePv 才下发。
|
|
|
*/
|
|
*/
|
|
|
static boolean passesExposureFilter(Long matchExposurePv, Long minExposurePv) {
|
|
static boolean passesExposureFilter(Long matchExposurePv, Long minExposurePv) {
|
|
|
if (minExposurePv == null || minExposurePv <= 0) {
|
|
if (minExposurePv == null || minExposurePv <= 0) {
|
|
@@ -731,15 +747,15 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
|
|
|
if (StringUtils.hasText(param.getTitle())) {
|
|
if (StringUtils.hasText(param.getTitle())) {
|
|
|
return searchByTitleInDemandPool(param, user);
|
|
return searchByTitleInDemandPool(param, user);
|
|
|
}
|
|
}
|
|
|
- String source = param.getSource();
|
|
|
|
|
- if (SOURCE_PRIOR.equalsIgnoreCase(source)) {
|
|
|
|
|
- return getSingleSourcePage(param, user, SOURCE_PRIOR);
|
|
|
|
|
|
|
+ VideoContentSource source = VideoContentSource.fromParam(param.getSource());
|
|
|
|
|
+ if (VideoContentSource.PRIOR == source) {
|
|
|
|
|
+ return getSingleSourcePage(param, user, source);
|
|
|
}
|
|
}
|
|
|
- if (SOURCE_POSTERIOR.equalsIgnoreCase(source)) {
|
|
|
|
|
- return getSingleSourcePage(param, user, SOURCE_POSTERIOR);
|
|
|
|
|
|
|
+ if (VideoContentSource.POSTERIOR == source) {
|
|
|
|
|
+ return getSingleSourcePage(param, user, source);
|
|
|
}
|
|
}
|
|
|
- if (SOURCE_HOT.equalsIgnoreCase(source)) {
|
|
|
|
|
- return getSingleSourcePage(param, user, SOURCE_HOT);
|
|
|
|
|
|
|
+ if (VideoContentSource.HOT == source) {
|
|
|
|
|
+ return getSingleSourcePage(param, user, source);
|
|
|
}
|
|
}
|
|
|
return getInterleavedPage(param, user);
|
|
return getInterleavedPage(param, user);
|
|
|
}
|
|
}
|
|
@@ -748,34 +764,35 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
|
|
|
* 单一来源分页:与穿插使用同一套候选构建逻辑(人群需求/优质相似各 2 阶段、组内 score top K),
|
|
* 单一来源分页:与穿插使用同一套候选构建逻辑(人群需求/优质相似各 2 阶段、组内 score top K),
|
|
|
* 再按 pageNum/pageSize 在内存中分页。totalSize = 去重后总数。
|
|
* 再按 pageNum/pageSize 在内存中分页。totalSize = 去重后总数。
|
|
|
*/
|
|
*/
|
|
|
- private Page<VideoContentItemVO> getSingleSourcePage(VideoContentListParam param, ContentPlatformAccount user, String source) {
|
|
|
|
|
- if (SOURCE_HOT.equals(source)) {
|
|
|
|
|
|
|
+ private Page<VideoContentItemVO> getSingleSourcePage(VideoContentListParam param, ContentPlatformAccount user, VideoContentSource source) {
|
|
|
|
|
+ if (VideoContentSource.HOT == source) {
|
|
|
return getHotSourcePaged(param, user);
|
|
return getHotSourcePaged(param, user);
|
|
|
}
|
|
}
|
|
|
List<VideoContentItemVO> list;
|
|
List<VideoContentItemVO> list;
|
|
|
- if (SOURCE_PRIOR.equals(source)) {
|
|
|
|
|
- // 粉丝喜欢:
|
|
|
|
|
- // 公众号入口(type∈{0,1,4}):3 池 — priorScene + prior(传播头部) + growth(增长头部),每位独立等概率抽 + seed=nanoTime
|
|
|
|
|
- // 企微入口 (type∈{2,3}):2 池 — priorScene + prior(传播头部),严格 1:1 交替(无随机)
|
|
|
|
|
- boolean isGzh = isGzhEntryType(param.getType());
|
|
|
|
|
- ExecutorService executor = Executors.newFixedThreadPool(isGzh ? 3 : 2);
|
|
|
|
|
|
|
+ if (VideoContentSource.PRIOR == source) {
|
|
|
|
|
+ // 人群需求池并行拉取, 按 PRIOR_POOL_CONFIGS 配置
|
|
|
|
|
+ int poolCount = PRIOR_POOL_CONFIGS.size();
|
|
|
|
|
+ ExecutorService executor = Executors.newFixedThreadPool(poolCount);
|
|
|
try {
|
|
try {
|
|
|
- Future<List<VideoContentItemVO>> fScene = executor.submit(
|
|
|
|
|
- () -> fetchPriorSceneCandidates(param, user, DEMAND_CANDIDATE_LIMIT));
|
|
|
|
|
- Future<List<VideoContentItemVO>> fPrior = executor.submit(
|
|
|
|
|
- () -> fetchPriorCandidates(param, user, DEMAND_CANDIDATE_LIMIT));
|
|
|
|
|
- Future<List<VideoContentItemVO>> fGrowth = isGzh ? executor.submit(
|
|
|
|
|
- () -> fetchPriorGrowthCandidates(param, user, DEMAND_CANDIDATE_LIMIT)) : null;
|
|
|
|
|
|
|
+ List<Future<List<VideoContentItemVO>>> futures = new ArrayList<>(poolCount);
|
|
|
|
|
+ for (PriorPoolConfig cfg : PRIOR_POOL_CONFIGS) {
|
|
|
|
|
+ futures.add(executor.submit(cfg.scene
|
|
|
|
|
+ ? () -> fetchPriorSceneCandidates(param, user, DEMAND_CANDIDATE_LIMIT, cfg.dimension, cfg.demandType, cfg.matchMethod)
|
|
|
|
|
+ : () -> fetchPriorPool(param, user, DEMAND_CANDIDATE_LIMIT, cfg.dimension, cfg.demandType, cfg.matchMethod)));
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
int timeoutSeconds = 30;
|
|
int timeoutSeconds = 30;
|
|
|
- List<VideoContentItemVO> scene = getQuietly(fScene, timeoutSeconds);
|
|
|
|
|
- List<VideoContentItemVO> prior = getQuietly(fPrior, timeoutSeconds);
|
|
|
|
|
- if (isGzh) {
|
|
|
|
|
- List<VideoContentItemVO> growth = getQuietly(fGrowth, timeoutSeconds);
|
|
|
|
|
- list = interleavePriorPoolsRandom(scene, prior, growth, user);
|
|
|
|
|
- } else {
|
|
|
|
|
- list = interleavePriorWithScene(scene, prior);
|
|
|
|
|
|
|
+ List<List<VideoContentItemVO>> pools = new ArrayList<>(poolCount);
|
|
|
|
|
+ for (Future<List<VideoContentItemVO>> f : futures) {
|
|
|
|
|
+ pools.add(getQuietly(f, timeoutSeconds));
|
|
|
|
|
+ }
|
|
|
|
|
+ for (List<VideoContentItemVO> pool : pools) {
|
|
|
|
|
+ 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);
|
|
|
} finally {
|
|
} finally {
|
|
|
executor.shutdown();
|
|
executor.shutdown();
|
|
|
}
|
|
}
|
|
@@ -783,17 +800,17 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
|
|
|
list = fetchPosteriorCandidates(param, user, DEMAND_CANDIDATE_LIMIT);
|
|
list = fetchPosteriorCandidates(param, user, DEMAND_CANDIDATE_LIMIT);
|
|
|
}
|
|
}
|
|
|
for (VideoContentItemVO v : list) {
|
|
for (VideoContentItemVO v : list) {
|
|
|
- v.setSource(source);
|
|
|
|
|
|
|
+ v.setSource(source.getValue());
|
|
|
}
|
|
}
|
|
|
return paginateCandidates(param, list);
|
|
return paginateCandidates(param, list);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* 3 池每位独立等概率抽样(公众号入口用):
|
|
* 3 池每位独立等概率抽样(公众号入口用):
|
|
|
- * - 池: [scene, prior(传播头部), growth(增长头部)]
|
|
|
|
|
- * - 每个输出位置在「未耗尽池」中等概率抽 1,从该池头部取下一条
|
|
|
|
|
- * - seed = nanoTime ^ userId:每次接口请求都换 seed,第一条来自哪一路每次都不同
|
|
|
|
|
- * - 跨池 video_id / 标题去重;翻页 P1/P2 不保证序列一致(刷新即换排)
|
|
|
|
|
|
|
+ * - 池: [scene, prior(传播头部), growth(增长头部)]
|
|
|
|
|
+ * - 每个输出位置在「未耗尽池」中等概率抽 1,从该池头部取下一条
|
|
|
|
|
+ * - seed = nanoTime ^ userId:每次接口请求都换 seed,第一条来自哪一路每次都不同
|
|
|
|
|
+ * - 跨池 video_id / 标题去重;翻页 P1/P2 不保证序列一致(刷新即换排)
|
|
|
*/
|
|
*/
|
|
|
private List<VideoContentItemVO> interleavePriorPoolsRandom(List<VideoContentItemVO> scene,
|
|
private List<VideoContentItemVO> interleavePriorPoolsRandom(List<VideoContentItemVO> scene,
|
|
|
List<VideoContentItemVO> prior,
|
|
List<VideoContentItemVO> prior,
|
|
@@ -806,8 +823,8 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* 企微入口用:priorScene 与 prior 池严格 1:1 交替输出(无随机):
|
|
* 企微入口用:priorScene 与 prior 池严格 1:1 交替输出(无随机):
|
|
|
- * - 起始池固定 scene,交替 1:1 各取 1 条
|
|
|
|
|
- * - 跨池 video_id / 标题去重;一侧用完后,剩余按原顺序追加输出,不丢数据
|
|
|
|
|
|
|
+ * - 起始池固定 scene,交替 1:1 各取 1 条
|
|
|
|
|
+ * - 跨池 video_id / 标题去重;一侧用完后,剩余按原顺序追加输出,不丢数据
|
|
|
*/
|
|
*/
|
|
|
private List<VideoContentItemVO> interleavePriorWithScene(List<VideoContentItemVO> scene,
|
|
private List<VideoContentItemVO> interleavePriorWithScene(List<VideoContentItemVO> scene,
|
|
|
List<VideoContentItemVO> prior) {
|
|
List<VideoContentItemVO> prior) {
|
|
@@ -830,9 +847,9 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* 通用 N 池随机穿插:
|
|
* 通用 N 池随机穿插:
|
|
|
- * - maxBlockSize=1 → 每位独立等概率从所有未耗尽池抽 1(允许连续同源)
|
|
|
|
|
- * - maxBlockSize=K(>=2) → 块大小 1~K 随机,块间切「其他未耗尽池」(避免连续同源)
|
|
|
|
|
- * - 跨池 video_id / 标题去重;某池跳过去重后耗尽即标记 exhausted
|
|
|
|
|
|
|
+ * - maxBlockSize=1 → 每位独立等概率从所有未耗尽池抽 1(允许连续同源)
|
|
|
|
|
+ * - maxBlockSize=K(>=2) → 块大小 1~K 随机,块间切「其他未耗尽池」(避免连续同源)
|
|
|
|
|
+ * - 跨池 video_id / 标题去重;某池跳过去重后耗尽即标记 exhausted
|
|
|
*/
|
|
*/
|
|
|
private List<VideoContentItemVO> interleaveMultiPools(List<List<VideoContentItemVO>> pools,
|
|
private List<VideoContentItemVO> interleaveMultiPools(List<List<VideoContentItemVO>> pools,
|
|
|
Random rng,
|
|
Random rng,
|
|
@@ -929,7 +946,7 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
|
|
|
return result;
|
|
return result;
|
|
|
}
|
|
}
|
|
|
String sort = getVideoContentListSort(param.getSort());
|
|
String sort = getVideoContentListSort(param.getSort());
|
|
|
- String type = getVideoContentListType(param.getType());
|
|
|
|
|
|
|
+ String type = param.getIndustryType().getDescription();
|
|
|
String channel = getVideoContentListChannel(param.getSort(), user.getChannel());
|
|
String channel = getVideoContentListChannel(param.getSort(), user.getChannel());
|
|
|
String strategy = param.getSort() == 3 ? "recommend" : "normal";
|
|
String strategy = param.getSort() == 3 ? "recommend" : "normal";
|
|
|
List<ContentPlatformVideo> videoList = planMapperExt.getVideoList(param, dt, datastatDt, type, channel, strategy,
|
|
List<ContentPlatformVideo> videoList = planMapperExt.getVideoList(param, dt, datastatDt, type, channel, strategy,
|
|
@@ -939,7 +956,7 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
|
|
|
list = new ArrayList<>();
|
|
list = new ArrayList<>();
|
|
|
}
|
|
}
|
|
|
for (VideoContentItemVO v : list) {
|
|
for (VideoContentItemVO v : list) {
|
|
|
- v.setSource(SOURCE_HOT);
|
|
|
|
|
|
|
+ v.setSource(VideoContentSource.HOT.getValue());
|
|
|
}
|
|
}
|
|
|
result.setObjs(list);
|
|
result.setObjs(list);
|
|
|
return result;
|
|
return result;
|
|
@@ -947,48 +964,42 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* 默认 tab 随机穿插 + 跨路 video_id 去重。
|
|
* 默认 tab 随机穿插 + 跨路 video_id 去重。
|
|
|
- * 公众号入口 5 池: priorScene / prior(传播头部) / priorGrowth(增长头部) / posterior / hot
|
|
|
|
|
- * 企微入口 4 池: priorScene / prior(传播头部) / posterior / hot(沿用旧逻辑,不加 growth)
|
|
|
|
|
- * prior 类(scene/prior/growth)对外都标 source='prior'(粉丝喜欢)。
|
|
|
|
|
|
|
+ * 人群需求池(source='prior') + 优质相似(source='posterior') + 热门(source='hot')
|
|
|
* 每步在未耗尽的池中等概率随机选一个,从该池头部取下一条(池内顺序由 fetcher 决定)。
|
|
* 每步在未耗尽的池中等概率随机选一个,从该池头部取下一条(池内顺序由 fetcher 决定)。
|
|
|
* 用 (userId ^ 当天日期) 作为种子,保证同一用户当天翻页顺序一致、刷新一致。
|
|
* 用 (userId ^ 当天日期) 作为种子,保证同一用户当天翻页顺序一致、刷新一致。
|
|
|
*/
|
|
*/
|
|
|
private Page<VideoContentItemVO> getInterleavedPage(VideoContentListParam param, ContentPlatformAccount user) {
|
|
private Page<VideoContentItemVO> getInterleavedPage(VideoContentListParam param, ContentPlatformAccount user) {
|
|
|
- boolean isGzh = isGzhEntryType(param.getType());
|
|
|
|
|
- int poolCount = isGzh ? 5 : 4;
|
|
|
|
|
- ExecutorService executor = Executors.newFixedThreadPool(poolCount);
|
|
|
|
|
|
|
+ int priorCount = PRIOR_POOL_CONFIGS.size();
|
|
|
|
|
+ int totalCount = priorCount + 2; // + posterior + hot
|
|
|
|
|
+ ExecutorService executor = Executors.newFixedThreadPool(totalCount);
|
|
|
try {
|
|
try {
|
|
|
- // 各池并行拉取,互不依赖
|
|
|
|
|
- Future<List<VideoContentItemVO>> fPriorScene = executor.submit(
|
|
|
|
|
- () -> fetchPriorSceneCandidates(param, user, DEMAND_CANDIDATE_LIMIT));
|
|
|
|
|
- Future<List<VideoContentItemVO>> fPrior = executor.submit(
|
|
|
|
|
- () -> fetchPriorCandidates(param, user, DEMAND_CANDIDATE_LIMIT));
|
|
|
|
|
|
|
+ // prior 池并行拉取
|
|
|
|
|
+ List<Future<List<VideoContentItemVO>>> futures = new ArrayList<>(totalCount);
|
|
|
|
|
+ for (PriorPoolConfig cfg : PRIOR_POOL_CONFIGS) {
|
|
|
|
|
+ futures.add(executor.submit(cfg.scene
|
|
|
|
|
+ ? () -> fetchPriorSceneCandidates(param, user, DEMAND_CANDIDATE_LIMIT, cfg.dimension, cfg.demandType, cfg.matchMethod)
|
|
|
|
|
+ : () -> fetchPriorPool(param, user, DEMAND_CANDIDATE_LIMIT, cfg.dimension, cfg.demandType, cfg.matchMethod)));
|
|
|
|
|
+ }
|
|
|
|
|
+ // posterior + hot
|
|
|
Future<List<VideoContentItemVO>> fPosterior = executor.submit(
|
|
Future<List<VideoContentItemVO>> fPosterior = executor.submit(
|
|
|
() -> fetchPosteriorCandidates(param, user, DEMAND_CANDIDATE_LIMIT));
|
|
() -> fetchPosteriorCandidates(param, user, DEMAND_CANDIDATE_LIMIT));
|
|
|
Future<List<VideoContentItemVO>> fHot = executor.submit(
|
|
Future<List<VideoContentItemVO>> fHot = executor.submit(
|
|
|
() -> fetchHotCandidates(param, user, HOT_CANDIDATE_LIMIT));
|
|
() -> fetchHotCandidates(param, user, HOT_CANDIDATE_LIMIT));
|
|
|
- Future<List<VideoContentItemVO>> fPriorGrowth = isGzh ? executor.submit(
|
|
|
|
|
- () -> fetchPriorGrowthCandidates(param, user, DEMAND_CANDIDATE_LIMIT)) : null;
|
|
|
|
|
|
|
+ futures.add(fPosterior);
|
|
|
|
|
+ futures.add(fHot);
|
|
|
|
|
|
|
|
int timeoutSeconds = 30;
|
|
int timeoutSeconds = 30;
|
|
|
- List<VideoContentItemVO> priorScene = getQuietly(fPriorScene, timeoutSeconds);
|
|
|
|
|
- List<VideoContentItemVO> prior = getQuietly(fPrior, timeoutSeconds);
|
|
|
|
|
- List<VideoContentItemVO> posterior = getQuietly(fPosterior, timeoutSeconds);
|
|
|
|
|
- List<VideoContentItemVO> hot = getQuietly(fHot, timeoutSeconds);
|
|
|
|
|
-
|
|
|
|
|
- for (VideoContentItemVO v : priorScene) v.setSource(SOURCE_PRIOR);
|
|
|
|
|
- for (VideoContentItemVO v : prior) v.setSource(SOURCE_PRIOR);
|
|
|
|
|
- for (VideoContentItemVO v : posterior) v.setSource(SOURCE_POSTERIOR);
|
|
|
|
|
- for (VideoContentItemVO v : hot) v.setSource(SOURCE_HOT);
|
|
|
|
|
-
|
|
|
|
|
- List<List<VideoContentItemVO>> pools;
|
|
|
|
|
- if (isGzh) {
|
|
|
|
|
- List<VideoContentItemVO> priorGrowth = getQuietly(fPriorGrowth, timeoutSeconds);
|
|
|
|
|
- for (VideoContentItemVO v : priorGrowth) v.setSource(SOURCE_PRIOR);
|
|
|
|
|
- pools = Arrays.asList(priorScene, prior, priorGrowth, posterior, hot);
|
|
|
|
|
- } else {
|
|
|
|
|
- pools = Arrays.asList(priorScene, prior, posterior, hot);
|
|
|
|
|
|
|
+ List<List<VideoContentItemVO>> pools = new ArrayList<>(totalCount);
|
|
|
|
|
+ for (int i = 0; i < futures.size(); i++) {
|
|
|
|
|
+ pools.add(getQuietly(futures.get(i), timeoutSeconds));
|
|
|
}
|
|
}
|
|
|
|
|
+ // source 标记: 前 priorCount 个池为 prior, 倒数第2为 posterior, 最后为 hot
|
|
|
|
|
+ for (int i = 0; i < priorCount; i++) {
|
|
|
|
|
+ for (VideoContentItemVO v : pools.get(i)) v.setSource(VideoContentSource.PRIOR.getValue());
|
|
|
|
|
+ }
|
|
|
|
|
+ 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 N = pools.size();
|
|
|
int[] pointers = new int[N];
|
|
int[] pointers = new int[N];
|
|
|
boolean[] exhausted = new boolean[N];
|
|
boolean[] exhausted = new boolean[N];
|
|
@@ -1003,7 +1014,10 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
|
|
|
while (true) {
|
|
while (true) {
|
|
|
boolean allExhausted = true;
|
|
boolean allExhausted = true;
|
|
|
for (boolean e : exhausted) {
|
|
for (boolean e : exhausted) {
|
|
|
- if (!e) { allExhausted = false; break; }
|
|
|
|
|
|
|
+ if (!e) {
|
|
|
|
|
+ allExhausted = false;
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
if (allExhausted) break;
|
|
if (allExhausted) break;
|
|
|
|
|
|
|
@@ -1061,33 +1075,38 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 人群需求-场景池: demand_strategy='人群需求-场景'。
|
|
|
|
|
|
|
+ * 人群需求-场景池: 按 dimension + demandType + matchMethod 组合过滤。
|
|
|
* 退化策略: ghName 非空且查不到数据 → 退回渠道粒度(不限 channel_level3)。
|
|
* 退化策略: ghName 非空且查不到数据 → 退回渠道粒度(不限 channel_level3)。
|
|
|
* 后处理:
|
|
* 后处理:
|
|
|
- * 1. 同 video_id 仅保留 total_rov 最大的代表行(利用 SQL 已按 total_rov DESC, score DESC 排好,首次即最大)
|
|
|
|
|
- * 2. 过滤 rov 为 null 或 <=0(视频近 7 日无表现)
|
|
|
|
|
- * 3. 输出顺序按 sceneSumRov DESC,相同再按 total_rov DESC 兜底
|
|
|
|
|
|
|
+ * 1. 同 video_id 仅保留 total_rov 最大的代表行(利用 SQL 已按 total_rov DESC, score DESC 排好,首次即最大)
|
|
|
|
|
+ * 2. 过滤 rov 为 null 或 <=0(视频近 7 日无表现)
|
|
|
|
|
+ * 3. 输出顺序按 sceneSumRov DESC,相同再按 total_rov DESC 兜底
|
|
|
*/
|
|
*/
|
|
|
- private List<VideoContentItemVO> fetchPriorSceneCandidates(VideoContentListParam param, ContentPlatformAccount user, int limit) {
|
|
|
|
|
- String channelName = resolveChannelName(param);
|
|
|
|
|
- String dt = demandVideoMapperExt.getMaxDt(channelName);
|
|
|
|
|
|
|
+ private List<VideoContentItemVO> fetchPriorSceneCandidates(VideoContentListParam param, ContentPlatformAccount user, int limit,
|
|
|
|
|
+ PriorDimensionEnum dimension, DemandTypeEnum demandType, DemandMatchMethodEnum matchMethod) {
|
|
|
|
|
+ DemandChannelEnum channel = resolveChannelName(param);
|
|
|
|
|
+ String dt = demandVideoMapperExt.getMaxDt(channel == null ? null : channel.getValue());
|
|
|
if (!StringUtils.hasText(dt)) {
|
|
if (!StringUtils.hasText(dt)) {
|
|
|
return new ArrayList<>();
|
|
return new ArrayList<>();
|
|
|
}
|
|
}
|
|
|
- String crowdSegment = resolveCrowdSegment(channelName, user);
|
|
|
|
|
|
|
+ String crowdSegment = resolveCrowdSegment(channel, user);
|
|
|
String ghName = StringUtils.hasText(param.getGhName()) ? param.getGhName() : null;
|
|
String ghName = StringUtils.hasText(param.getGhName()) ? param.getGhName() : null;
|
|
|
|
|
|
|
|
String category = StringUtils.hasText(param.getCategory()) ? param.getCategory() : null;
|
|
String category = StringUtils.hasText(param.getCategory()) ? param.getCategory() : null;
|
|
|
String crowdPackage = StringUtils.hasText(param.getCrowdPackage()) ? param.getCrowdPackage() : null;
|
|
String crowdPackage = StringUtils.hasText(param.getCrowdPackage()) ? param.getCrowdPackage() : null;
|
|
|
- List<String> excludeCategories = resolveExcludeCategories(channelName, user);
|
|
|
|
|
- // priorScene 池新识别:demand_strategy='人群需求' AND match_method='场景已看视频'(0519+ 起,旧 demand_strategy='人群需求-场景' 已迁走)
|
|
|
|
|
|
|
+ List<String> excludeCategories = resolveExcludeCategories(channel, user);
|
|
|
|
|
+ String ch = channel == null ? null : channel.getValue();
|
|
|
List<ContentPlatformDemandVideo> rows = demandVideoMapperExt.selectForRecommend(
|
|
List<ContentPlatformDemandVideo> rows = demandVideoMapperExt.selectForRecommend(
|
|
|
- dt, channelName, crowdSegment, DEMAND_STRATEGY_PRIOR, null, null, null, ghName, null, category, MATCH_METHOD_PRIOR_SCENE, crowdPackage, limit, false, excludeCategories);
|
|
|
|
|
- // 跨渠道退化:仅合作类渠道(企微/即转/Daily-自选)主查 crowd_segment 0 行时去掉合作方代码再试一次,
|
|
|
|
|
|
|
+ dt, ch, crowdSegment, DemandStrategyEnum.PRIOR.getValue(), dimension.getValue(),
|
|
|
|
|
+ null, null, ghName, null, category,
|
|
|
|
|
+ matchMethod.getValue(), demandType.getValue(), crowdPackage, limit, false, excludeCategories);
|
|
|
|
|
+ // 跨渠道退化:仅合作类渠道主查 crowd_segment 0 行时去掉合作方代码再试一次,
|
|
|
// 仍保留 ghName,避免公众号入口结果跨账号串号。投流/服务号类渠道 crowdSegment 已是 null,主查与本路径等价,不会进入。
|
|
// 仍保留 ghName,避免公众号入口结果跨账号串号。投流/服务号类渠道 crowdSegment 已是 null,主查与本路径等价,不会进入。
|
|
|
- if (channelName != null && crowdSegment != null && rows.isEmpty()) {
|
|
|
|
|
|
|
+ if (channel != null && crowdSegment != null && rows.isEmpty()) {
|
|
|
rows = demandVideoMapperExt.selectForRecommend(
|
|
rows = demandVideoMapperExt.selectForRecommend(
|
|
|
- dt, channelName, null, DEMAND_STRATEGY_PRIOR, null, null, null, ghName, null, category, MATCH_METHOD_PRIOR_SCENE, crowdPackage, limit, false, excludeCategories);
|
|
|
|
|
|
|
+ dt, ch, null, DemandStrategyEnum.PRIOR.getValue(), dimension.getValue(),
|
|
|
|
|
+ null, null, ghName, null, category,
|
|
|
|
|
+ matchMethod.getValue(), demandType.getValue(), crowdPackage, limit, false, excludeCategories);
|
|
|
}
|
|
}
|
|
|
// 1. 同 video_id 取 total_rov 最大的代表行(SQL 已排序,putIfAbsent 保留首次)
|
|
// 1. 同 video_id 取 total_rov 最大的代表行(SQL 已排序,putIfAbsent 保留首次)
|
|
|
LinkedHashMap<Long, ContentPlatformDemandVideo> bestPerVideo = new LinkedHashMap<>();
|
|
LinkedHashMap<Long, ContentPlatformDemandVideo> bestPerVideo = new LinkedHashMap<>();
|
|
@@ -1095,11 +1114,11 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
|
|
|
if (r.getVideoId() == null) continue;
|
|
if (r.getVideoId() == null) continue;
|
|
|
bestPerVideo.putIfAbsent(r.getVideoId(), r);
|
|
bestPerVideo.putIfAbsent(r.getVideoId(), r);
|
|
|
}
|
|
}
|
|
|
- // 2. 过滤 rov<=0/null,以及 sceneSumRov < PRIOR_SCENE_MIN_SUM_ROV(场景关联弱视频砍掉)
|
|
|
|
|
|
|
+ // 2. 过滤 rov<=0/null,以及 sceneSumRov < priorSceneMinSumRov(场景关联弱视频砍掉)
|
|
|
List<ContentPlatformDemandVideo> filtered = new ArrayList<>(bestPerVideo.size());
|
|
List<ContentPlatformDemandVideo> filtered = new ArrayList<>(bestPerVideo.size());
|
|
|
for (ContentPlatformDemandVideo r : bestPerVideo.values()) {
|
|
for (ContentPlatformDemandVideo r : bestPerVideo.values()) {
|
|
|
if (r.getRov() == null || r.getRov() <= 0) continue;
|
|
if (r.getRov() == null || r.getRov() <= 0) continue;
|
|
|
- if (r.getSceneSumRov() == null || r.getSceneSumRov() < PRIOR_SCENE_MIN_SUM_ROV) continue;
|
|
|
|
|
|
|
+ if (r.getSceneSumRov() == null || r.getSceneSumRov() < priorSceneMinSumRov) continue;
|
|
|
// 曝光下发过滤:场景已看视频池 match_exposure_pv 100% 为 NULL,此处恒放行;保留以统一全池口径。
|
|
// 曝光下发过滤:场景已看视频池 match_exposure_pv 100% 为 NULL,此处恒放行;保留以统一全池口径。
|
|
|
if (!passesExposureFilter(r.getMatchExposurePv(), demandMinExposurePv)) continue;
|
|
if (!passesExposureFilter(r.getMatchExposurePv(), demandMinExposurePv)) continue;
|
|
|
filtered.add(r);
|
|
filtered.add(r);
|
|
@@ -1118,54 +1137,46 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 人群需求池(prior):dimension='传播的头部'。
|
|
|
|
|
- * 复用 fetchPriorDimensionCandidates 的 pipeline,仅 dimension 不同。
|
|
|
|
|
- */
|
|
|
|
|
- private List<VideoContentItemVO> fetchPriorCandidates(VideoContentListParam param, ContentPlatformAccount user, int limit) {
|
|
|
|
|
- return fetchPriorDimensionCandidates(param, user, limit, PRIOR_PREMIUM_DIMENSION);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * 人群需求池(priorGrowth):dimension='增长的头部'。
|
|
|
|
|
- * 与 prior 池(传播头部)分开各算各的 top 50% 分位,二者跨池 video_id 去重在 interleave 阶段处理。
|
|
|
|
|
|
|
+ * 人群需求池通用 pipeline(非 scene 池,即策略2/3/4 共 9 池):
|
|
|
|
|
+ * 1. SQL: demand_strategy='人群需求' + dimension + demandType + matchMethod
|
|
|
|
|
+ * 2. 退化: ghName 无数据 → 去 ghName; crowd_segment 在对侧渠道 0 行 → 去 crowd_segment
|
|
|
|
|
+ * 3. 近 7 日 rov >= demandMinRov 过滤
|
|
|
|
|
+ * 4. 按 (point_type, standard_element) 分组,按 total_rov 分位保留 top 50%
|
|
|
|
|
+ * 5. 组按 total_rov DESC、组内 score DESC, top K=3
|
|
|
|
|
+ * 6. 截断到 limit
|
|
|
*/
|
|
*/
|
|
|
- private List<VideoContentItemVO> fetchPriorGrowthCandidates(VideoContentListParam param, ContentPlatformAccount user, int limit) {
|
|
|
|
|
- return fetchPriorDimensionCandidates(param, user, limit, GROWTH_PREMIUM_DIMENSION);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * 人群需求池共用 pipeline,按 dimension 切分:
|
|
|
|
|
- * 1. SQL: demand_strategy='人群需求' + dimension=<指定> + match_method='视频库_解构特征_向量相似匹配'
|
|
|
|
|
- * 2. 退化:ghName 无数据 → 去 ghName;crowd_segment 在对侧渠道 0 行 → 去 crowd_segment
|
|
|
|
|
- * 3. 近 7 日 rov >= DEMAND_MIN_ROV 过滤
|
|
|
|
|
- * 4. 按 (point_type, standard_element) 分组,按 total_rov 分位保留 top 50%
|
|
|
|
|
- * 5. 组按 total_rov DESC、组内 score DESC,top K=3
|
|
|
|
|
- * 6. 截断到 limit
|
|
|
|
|
- */
|
|
|
|
|
- private List<VideoContentItemVO> fetchPriorDimensionCandidates(VideoContentListParam param, ContentPlatformAccount user, int limit, String dimension) {
|
|
|
|
|
- String channelName = resolveChannelName(param);
|
|
|
|
|
- String dt = demandVideoMapperExt.getMaxDt(channelName);
|
|
|
|
|
|
|
+ private List<VideoContentItemVO> fetchPriorPool(VideoContentListParam param, ContentPlatformAccount user, int limit,
|
|
|
|
|
+ PriorDimensionEnum dimension, DemandTypeEnum demandType, DemandMatchMethodEnum matchMethod) {
|
|
|
|
|
+ DemandChannelEnum channel = resolveChannelName(param);
|
|
|
|
|
+ String dt = demandVideoMapperExt.getMaxDt(channel == null ? null : channel.getValue());
|
|
|
if (!StringUtils.hasText(dt)) {
|
|
if (!StringUtils.hasText(dt)) {
|
|
|
return new ArrayList<>();
|
|
return new ArrayList<>();
|
|
|
}
|
|
}
|
|
|
- String crowdSegment = resolveCrowdSegment(channelName, user);
|
|
|
|
|
|
|
+ String crowdSegment = resolveCrowdSegment(channel, user);
|
|
|
String ghName = StringUtils.hasText(param.getGhName()) ? param.getGhName() : null;
|
|
String ghName = StringUtils.hasText(param.getGhName()) ? param.getGhName() : null;
|
|
|
int fetchLimit = Math.max(limit * 3, DEMAND_CANDIDATE_LIMIT);
|
|
int fetchLimit = Math.max(limit * 3, DEMAND_CANDIDATE_LIMIT);
|
|
|
|
|
|
|
|
String category = StringUtils.hasText(param.getCategory()) ? param.getCategory() : null;
|
|
String category = StringUtils.hasText(param.getCategory()) ? param.getCategory() : null;
|
|
|
String crowdPackage = StringUtils.hasText(param.getCrowdPackage()) ? param.getCrowdPackage() : null;
|
|
String crowdPackage = StringUtils.hasText(param.getCrowdPackage()) ? param.getCrowdPackage() : null;
|
|
|
- List<String> excludeCategories = resolveExcludeCategories(channelName, user);
|
|
|
|
|
|
|
+ List<String> excludeCategories = resolveExcludeCategories(channel, user);
|
|
|
|
|
+ String ch = channel == null ? null : channel.getValue();
|
|
|
List<ContentPlatformDemandVideo> rows = demandVideoMapperExt.selectForRecommend(
|
|
List<ContentPlatformDemandVideo> rows = demandVideoMapperExt.selectForRecommend(
|
|
|
- dt, channelName, crowdSegment, DEMAND_STRATEGY_PRIOR, dimension, null, null, ghName, null, category, MATCH_METHOD_PRIOR, crowdPackage, fetchLimit, false, excludeCategories);
|
|
|
|
|
|
|
+ dt, ch, crowdSegment, DemandStrategyEnum.PRIOR.getValue(), dimension.getValue(),
|
|
|
|
|
+ null, null, ghName, null, category,
|
|
|
|
|
+ matchMethod.getValue(), demandType.getValue(), crowdPackage, fetchLimit, false,
|
|
|
|
|
+ excludeCategories);
|
|
|
|
|
|
|
|
- // 跨渠道退化:见 fetchPriorSceneCandidates 注释。仅合作类渠道触发,保留 ghName 防串号。
|
|
|
|
|
- if (channelName != null && crowdSegment != null && rows.isEmpty()) {
|
|
|
|
|
|
|
+ // 跨渠道退化:仅合作类渠道触发,保留 ghName 防串号。
|
|
|
|
|
+ if (channel != null && crowdSegment != null && rows.isEmpty()) {
|
|
|
rows = demandVideoMapperExt.selectForRecommend(
|
|
rows = demandVideoMapperExt.selectForRecommend(
|
|
|
- dt, channelName, null, DEMAND_STRATEGY_PRIOR, dimension, null, null, ghName, null, category, MATCH_METHOD_PRIOR, crowdPackage, fetchLimit, false, excludeCategories);
|
|
|
|
|
|
|
+ dt, ch, null, DemandStrategyEnum.PRIOR.getValue(), dimension.getValue(),
|
|
|
|
|
+ null, null, ghName, null, category,
|
|
|
|
|
+ matchMethod.getValue(), demandType.getValue(), crowdPackage, fetchLimit, false,
|
|
|
|
|
+ excludeCategories);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
rows = rows.stream()
|
|
rows = rows.stream()
|
|
|
- .filter(r -> r.getRov() != null && r.getRov() >= DEMAND_MIN_ROV)
|
|
|
|
|
|
|
+ .filter(r -> r.getRov() != null && r.getRov() >= demandMinRov)
|
|
|
.filter(r -> passesExposureFilter(r.getMatchExposurePv(), demandMinExposurePv))
|
|
.filter(r -> passesExposureFilter(r.getMatchExposurePv(), demandMinExposurePv))
|
|
|
.collect(Collectors.toList());
|
|
.collect(Collectors.toList());
|
|
|
|
|
|
|
@@ -1173,7 +1184,7 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
|
|
|
(r.getPointType() == null ? "" : r.getPointType()) + "\u0001"
|
|
(r.getPointType() == null ? "" : r.getPointType()) + "\u0001"
|
|
|
+ (r.getStandardElement() == null ? "" : r.getStandardElement());
|
|
+ (r.getStandardElement() == null ? "" : r.getStandardElement());
|
|
|
|
|
|
|
|
- rows = retainTopGroupsByTotalRov(rows, keyFn, PRIOR_GROUP_KEEP_RATIO);
|
|
|
|
|
|
|
+ rows = retainTopGroupsByTotalRov(rows, keyFn, priorGroupKeepRatio);
|
|
|
|
|
|
|
|
List<VideoContentItemVO> out = groupAndTopK(rows, keyFn, TOP_K_PER_DEMAND, false);
|
|
List<VideoContentItemVO> out = groupAndTopK(rows, keyFn, TOP_K_PER_DEMAND, false);
|
|
|
if (out.size() > limit) {
|
|
if (out.size() > limit) {
|
|
@@ -1221,32 +1232,38 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
|
|
|
* 跨组用 video_id + 归一化标题去重,截到 limit。
|
|
* 跨组用 video_id + 归一化标题去重,截到 limit。
|
|
|
*/
|
|
*/
|
|
|
private List<VideoContentItemVO> fetchPosteriorCandidates(VideoContentListParam param, ContentPlatformAccount user, int limit) {
|
|
private List<VideoContentItemVO> fetchPosteriorCandidates(VideoContentListParam param, ContentPlatformAccount user, int limit) {
|
|
|
- String channelName = resolveChannelName(param);
|
|
|
|
|
- String dt = demandVideoMapperExt.getMaxDt(channelName);
|
|
|
|
|
|
|
+ DemandChannelEnum channel = resolveChannelName(param);
|
|
|
|
|
+ String dt = demandVideoMapperExt.getMaxDt(channel == null ? null : channel.getValue());
|
|
|
if (!StringUtils.hasText(dt)) {
|
|
if (!StringUtils.hasText(dt)) {
|
|
|
return new ArrayList<>();
|
|
return new ArrayList<>();
|
|
|
}
|
|
}
|
|
|
- String crowdSegment = resolveCrowdSegment(channelName, user);
|
|
|
|
|
|
|
+ String crowdSegment = resolveCrowdSegment(channel, user);
|
|
|
String ghName = StringUtils.hasText(param.getGhName()) ? param.getGhName() : null;
|
|
String ghName = StringUtils.hasText(param.getGhName()) ? param.getGhName() : null;
|
|
|
int fetchLimit = Math.max(limit * 3, DEMAND_CANDIDATE_LIMIT);
|
|
int fetchLimit = Math.max(limit * 3, DEMAND_CANDIDATE_LIMIT);
|
|
|
|
|
|
|
|
String category = StringUtils.hasText(param.getCategory()) ? param.getCategory() : null;
|
|
String category = StringUtils.hasText(param.getCategory()) ? param.getCategory() : null;
|
|
|
String crowdPackage = StringUtils.hasText(param.getCrowdPackage()) ? param.getCrowdPackage() : null;
|
|
String crowdPackage = StringUtils.hasText(param.getCrowdPackage()) ? param.getCrowdPackage() : null;
|
|
|
- List<String> excludeCategories = resolveExcludeCategories(channelName, user);
|
|
|
|
|
|
|
+ List<String> excludeCategories = resolveExcludeCategories(channel, user);
|
|
|
|
|
+ String ch = channel == null ? null : channel.getValue();
|
|
|
// posterior 池加 match_method='视频库_解构特征_向量相似匹配' 兜底,防止未来上游对优质相似分量出别的 match_method 值后污染本池
|
|
// posterior 池加 match_method='视频库_解构特征_向量相似匹配' 兜底,防止未来上游对优质相似分量出别的 match_method 值后污染本池
|
|
|
- // 优质相似池:drive_dimension_time 一律不限制(含主查与退化路径),避免仅「昨日」窗口召回过少。
|
|
|
|
|
List<ContentPlatformDemandVideo> rows = demandVideoMapperExt.selectForRecommend(
|
|
List<ContentPlatformDemandVideo> rows = demandVideoMapperExt.selectForRecommend(
|
|
|
- dt, channelName, crowdSegment, DEMAND_STRATEGY_POSTERIOR, null, null, null, ghName, null, category, MATCH_METHOD_PRIOR, crowdPackage, fetchLimit, true, excludeCategories);
|
|
|
|
|
|
|
+ dt, ch, crowdSegment, DemandStrategyEnum.POSTERIOR.getValue(),
|
|
|
|
|
+ null, null, null, ghName, null,
|
|
|
|
|
+ category, DemandMatchMethodEnum.VECTOR_SIMILARITY.getValue(), null, crowdPackage,
|
|
|
|
|
+ fetchLimit, true, excludeCategories);
|
|
|
|
|
|
|
|
// 跨渠道退化:见 fetchPriorSceneCandidates 注释。仅合作类渠道触发,保留 ghName 防串号。
|
|
// 跨渠道退化:见 fetchPriorSceneCandidates 注释。仅合作类渠道触发,保留 ghName 防串号。
|
|
|
- if (channelName != null && crowdSegment != null && rows.isEmpty()) {
|
|
|
|
|
|
|
+ if (channel != null && crowdSegment != null && rows.isEmpty()) {
|
|
|
rows = demandVideoMapperExt.selectForRecommend(
|
|
rows = demandVideoMapperExt.selectForRecommend(
|
|
|
- dt, channelName, null, DEMAND_STRATEGY_POSTERIOR, null, null, null, ghName, null, category, MATCH_METHOD_PRIOR, crowdPackage, fetchLimit, true, excludeCategories);
|
|
|
|
|
|
|
+ dt, ch, null, DemandStrategyEnum.POSTERIOR.getValue(),
|
|
|
|
|
+ null, null, null, ghName, null,
|
|
|
|
|
+ category, DemandMatchMethodEnum.VECTOR_SIMILARITY.getValue(), null, crowdPackage,
|
|
|
|
|
+ fetchLimit, true, excludeCategories);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 近 7 日 rov 下限,与 prior 池一致(DEMAND_MIN_ROV,统一到 0.03)
|
|
|
|
|
|
|
+ // 近 7 日 rov 下限,与 prior 池一致(demandMinRov,统一到 0.03)
|
|
|
rows = rows.stream()
|
|
rows = rows.stream()
|
|
|
- .filter(r -> r.getRov() != null && r.getRov() >= DEMAND_MIN_ROV)
|
|
|
|
|
|
|
+ .filter(r -> r.getRov() != null && r.getRov() >= demandMinRov)
|
|
|
.filter(r -> passesExposureFilter(r.getMatchExposurePv(), demandMinExposurePv))
|
|
.filter(r -> passesExposureFilter(r.getMatchExposurePv(), demandMinExposurePv))
|
|
|
.collect(Collectors.toList());
|
|
.collect(Collectors.toList());
|
|
|
|
|
|
|
@@ -1255,7 +1272,7 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
|
|
|
|
|
|
|
|
// 按 demand_content_id 的 total_rov 中位数过滤:保留中位数及以上(top 50%)的 demand 组,
|
|
// 按 demand_content_id 的 total_rov 中位数过滤:保留中位数及以上(top 50%)的 demand 组,
|
|
|
// 砍掉群体表现弱的需求,避免低 total_rov 的 demand 带回来的相似变体稀释结果。
|
|
// 砍掉群体表现弱的需求,避免低 total_rov 的 demand 带回来的相似变体稀释结果。
|
|
|
- rows = retainTopGroupsByTotalRov(rows, keyFn, POSTERIOR_GROUP_KEEP_RATIO);
|
|
|
|
|
|
|
+ rows = retainTopGroupsByTotalRov(rows, keyFn, posteriorGroupKeepRatio);
|
|
|
|
|
|
|
|
List<VideoContentItemVO> out = groupAndTopK(rows, keyFn, TOP_K_PER_DEMAND, true);
|
|
List<VideoContentItemVO> out = groupAndTopK(rows, keyFn, TOP_K_PER_DEMAND, true);
|
|
|
// 单段也要去归一化标题重复(同段内运营把同内容上传成多 video_id 的情况)
|
|
// 单段也要去归一化标题重复(同段内运营把同内容上传成多 video_id 的情况)
|
|
@@ -1264,10 +1281,10 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* 行级按 keyFn 分组:
|
|
* 行级按 keyFn 分组:
|
|
|
- * 1. 组按"组内最大 total_rov" 倒序
|
|
|
|
|
- * 2. 组内按 score 倒序,组内 video_id 去重,最多取 topK 条
|
|
|
|
|
- * 3. excludeSelfTitle=true 时先在 Java 端用 TitleNormalizer 过滤自标题行
|
|
|
|
|
- * 4. rov 为 null 或 <=0 的行视为"近 7 日无表现",直接丢弃不入池
|
|
|
|
|
|
|
+ * 1. 组按"组内最大 total_rov" 倒序
|
|
|
|
|
+ * 2. 组内按 score 倒序,组内 video_id 去重,最多取 topK 条
|
|
|
|
|
+ * 3. excludeSelfTitle=true 时先在 Java 端用 TitleNormalizer 过滤自标题行
|
|
|
|
|
+ * 4. rov 为 null 或 <=0 的行视为"近 7 日无表现",直接丢弃不入池
|
|
|
*/
|
|
*/
|
|
|
private List<VideoContentItemVO> groupAndTopK(List<ContentPlatformDemandVideo> rows,
|
|
private List<VideoContentItemVO> groupAndTopK(List<ContentPlatformDemandVideo> rows,
|
|
|
Function<ContentPlatformDemandVideo, String> keyFn,
|
|
Function<ContentPlatformDemandVideo, String> keyFn,
|
|
@@ -1337,7 +1354,7 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
|
|
|
return new ArrayList<>();
|
|
return new ArrayList<>();
|
|
|
}
|
|
}
|
|
|
String sort = getVideoContentListSort(param.getSort());
|
|
String sort = getVideoContentListSort(param.getSort());
|
|
|
- String type = getVideoContentListType(param.getType());
|
|
|
|
|
|
|
+ String type = param.getIndustryType().getDescription();
|
|
|
String channel = getVideoContentListChannel(param.getSort(), user.getChannel());
|
|
String channel = getVideoContentListChannel(param.getSort(), user.getChannel());
|
|
|
String strategy = param.getSort() == 3 ? "recommend" : "normal";
|
|
String strategy = param.getSort() == 3 ? "recommend" : "normal";
|
|
|
List<ContentPlatformVideo> videoList = planMapperExt.getVideoList(param, dt, datastatDt, type, channel, strategy,
|
|
List<ContentPlatformVideo> videoList = planMapperExt.getVideoList(param, dt, datastatDt, type, channel, strategy,
|
|
@@ -1511,29 +1528,6 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
|
|
|
return result;
|
|
return result;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /** type ∈ {0 自动回复, 1 服务号推送, 4 公众号推送} 为公众号入口;{2,3} 为企微。type=null/其他 视为非公众号(走企微稳定逻辑)。 */
|
|
|
|
|
- private boolean isGzhEntryType(Integer type) {
|
|
|
|
|
- if (type == null) return false;
|
|
|
|
|
- return type == 0 || type == 1 || type == 4;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private String getVideoContentListType(Integer type) {
|
|
|
|
|
- switch (type) {
|
|
|
|
|
- case 0:
|
|
|
|
|
- return "自动回复";
|
|
|
|
|
- case 1:
|
|
|
|
|
- return "服务号推送";
|
|
|
|
|
- case 2:
|
|
|
|
|
- return "企微-社群";
|
|
|
|
|
- case 3:
|
|
|
|
|
- return "企微-自动回复";
|
|
|
|
|
- case 4:
|
|
|
|
|
- return "公众号推送";
|
|
|
|
|
- default:
|
|
|
|
|
- return "";
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
private String getVideoContentListChannel(Integer sort, String channel) {
|
|
private String getVideoContentListChannel(Integer sort, String channel) {
|
|
|
if (sort == 2 || sort == 3) {
|
|
if (sort == 2 || sort == 3) {
|
|
|
return channel;
|
|
return channel;
|
|
@@ -1634,7 +1628,7 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
|
|
|
return result;
|
|
return result;
|
|
|
}
|
|
}
|
|
|
String sort = getVideoContentListSort(param.getSort());
|
|
String sort = getVideoContentListSort(param.getSort());
|
|
|
- String type = getVideoContentListType(param.getType());
|
|
|
|
|
|
|
+ String type = param.getIndustryType().getDescription();
|
|
|
String channel = getVideoContentListChannel(param.getSort(), user.getChannel());
|
|
String channel = getVideoContentListChannel(param.getSort(), user.getChannel());
|
|
|
List<ContentPlatformVideo> videoList = planMapperExt.getUploadVideoList(param, user.getId(), datastatDt,
|
|
List<ContentPlatformVideo> videoList = planMapperExt.getUploadVideoList(param, user.getId(), datastatDt,
|
|
|
type, channel, "normal", offset, param.getPageSize(), sort);
|
|
type, channel, "normal", offset, param.getPageSize(), sort);
|
|
@@ -2096,31 +2090,31 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
|
|
|
/**
|
|
/**
|
|
|
* 搜索:在当前入口的 demand 白名单(rov >= 0.03)内做 title LIKE 关键字命中,按 rov DESC 排序。
|
|
* 搜索:在当前入口的 demand 白名单(rov >= 0.03)内做 title LIKE 关键字命中,按 rov DESC 排序。
|
|
|
* 门控:
|
|
* 门控:
|
|
|
- * - 用户身份 type ∈ {2 自营, 3 代理},否则返回空
|
|
|
|
|
- * - 渠道 ∈ {小程序投流-稳定, 公众号投流-稳定},否则返回空
|
|
|
|
|
- * - 小程序投流必填 crowdPackage,公众号投流必填 ghName,否则返回空
|
|
|
|
|
|
|
+ * - 用户身份 type ∈ {2 自营, 3 代理},否则返回空
|
|
|
|
|
+ * - 渠道 ∈ {小程序投流-稳定, 公众号投流-稳定},否则返回空
|
|
|
|
|
+ * - 小程序投流必填 crowdPackage,公众号投流必填 ghName,否则返回空
|
|
|
* 流程:
|
|
* 流程:
|
|
|
- * 1. demand 白名单(channel + crowdSegment/channelLevel3 + demand_strategy IN (人群需求,优质相似) + rov>=0.03)
|
|
|
|
|
- * SQL 已 ORDER BY rov DESC, id ASC
|
|
|
|
|
- * 2. 同 video_id 去重:putIfAbsent 取 max rov 代表行 —— 与搜索 rov DESC 排序对齐,
|
|
|
|
|
- * 与 priorScene 池(取 max sceneSumRov 代表行)有意区别;同 dt 内稳定,跨 dt 重灌可能换代表行。
|
|
|
|
|
- * 3. title.trim().toLowerCase().contains(query) 关键字命中
|
|
|
|
|
- * 4. 排序:rov DESC,次级 total_rov DESC,score 字段清空避免前端歧义
|
|
|
|
|
- * 5. 按 pageNum/pageSize 内存切片返回
|
|
|
|
|
|
|
+ * 1. demand 白名单(channel + crowdSegment/channelLevel3 + demand_strategy IN (人群需求,优质相似) + rov>=0.03)
|
|
|
|
|
+ * SQL 已 ORDER BY rov DESC, id ASC
|
|
|
|
|
+ * 2. 同 video_id 去重:putIfAbsent 取 max rov 代表行 —— 与搜索 rov DESC 排序对齐,
|
|
|
|
|
+ * 与 priorScene 池(取 max sceneSumRov 代表行)有意区别;同 dt 内稳定,跨 dt 重灌可能换代表行。
|
|
|
|
|
+ * 3. title.trim().toLowerCase().contains(query) 关键字命中
|
|
|
|
|
+ * 4. 排序:rov DESC,次级 total_rov DESC,score 字段清空避免前端歧义
|
|
|
|
|
+ * 5. 按 pageNum/pageSize 内存切片返回
|
|
|
*/
|
|
*/
|
|
|
private Page<VideoContentItemVO> searchByTitleInDemandPool(VideoContentListParam param, ContentPlatformAccount user) {
|
|
private Page<VideoContentItemVO> searchByTitleInDemandPool(VideoContentListParam param, ContentPlatformAccount user) {
|
|
|
Page<VideoContentItemVO> empty = new Page<>(param.getPageNum(), param.getPageSize());
|
|
Page<VideoContentItemVO> empty = new Page<>(param.getPageNum(), param.getPageSize());
|
|
|
empty.setTotalSize(0);
|
|
empty.setTotalSize(0);
|
|
|
empty.setObjs(new ArrayList<>());
|
|
empty.setObjs(new ArrayList<>());
|
|
|
|
|
|
|
|
- if (user == null || !USER_TYPES_ALLOW_SEARCH.contains(user.getType())) {
|
|
|
|
|
|
|
+ if (user == null || !ContentPlatformAccountTypeEnum.from(user.getType()).allowSearch()) {
|
|
|
return empty;
|
|
return empty;
|
|
|
}
|
|
}
|
|
|
- String channelName = resolveChannelName(param);
|
|
|
|
|
- if (channelName == null || !CHANNELS_ALLOW_SEARCH.contains(channelName)) {
|
|
|
|
|
|
|
+ DemandChannelEnum channel = resolveChannelName(param);
|
|
|
|
|
+ if (channel == null || !channel.allowSearch()) {
|
|
|
return empty;
|
|
return empty;
|
|
|
}
|
|
}
|
|
|
- boolean isXcx = CHANNEL_NAME_XCX.equals(channelName);
|
|
|
|
|
|
|
+ boolean isXcx = DemandChannelEnum.XCX == channel;
|
|
|
String crowdPackage = StringUtils.hasText(param.getCrowdPackage()) ? param.getCrowdPackage() : null;
|
|
String crowdPackage = StringUtils.hasText(param.getCrowdPackage()) ? param.getCrowdPackage() : null;
|
|
|
String ghName = StringUtils.hasText(param.getGhName()) ? param.getGhName() : null;
|
|
String ghName = StringUtils.hasText(param.getGhName()) ? param.getGhName() : null;
|
|
|
if (isXcx && crowdPackage == null) return empty;
|
|
if (isXcx && crowdPackage == null) return empty;
|
|
@@ -2129,11 +2123,12 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
|
|
|
String kw = param.getTitle() == null ? "" : param.getTitle().trim().toLowerCase();
|
|
String kw = param.getTitle() == null ? "" : param.getTitle().trim().toLowerCase();
|
|
|
if (kw.isEmpty()) return empty;
|
|
if (kw.isEmpty()) return empty;
|
|
|
|
|
|
|
|
- String dt = demandVideoMapperExt.getMaxDt(channelName);
|
|
|
|
|
|
|
+ String ch = channel.getValue();
|
|
|
|
|
+ String dt = demandVideoMapperExt.getMaxDt(ch);
|
|
|
if (!StringUtils.hasText(dt)) return empty;
|
|
if (!StringUtils.hasText(dt)) return empty;
|
|
|
|
|
|
|
|
List<ContentPlatformDemandVideo> whitelist = demandVideoMapperExt.selectSearchWhitelist(
|
|
List<ContentPlatformDemandVideo> whitelist = demandVideoMapperExt.selectSearchWhitelist(
|
|
|
- dt, channelName, isXcx ? crowdPackage : null, isXcx ? null : ghName);
|
|
|
|
|
|
|
+ dt, ch, isXcx ? crowdPackage : null, isXcx ? null : ghName);
|
|
|
if (CollectionUtils.isEmpty(whitelist)) return empty;
|
|
if (CollectionUtils.isEmpty(whitelist)) return empty;
|
|
|
|
|
|
|
|
// SQL 已 ORDER BY rov DESC,putIfAbsent 即拿到 max rov 代表行(设计意图:与排序键一致)
|
|
// SQL 已 ORDER BY rov DESC,putIfAbsent 即拿到 max rov 代表行(设计意图:与排序键一致)
|
|
@@ -2442,11 +2437,11 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
|
|
|
|
|
|
|
|
@Override
|
|
@Override
|
|
|
public List<String> getXcxAudiencePackageList() {
|
|
public List<String> getXcxAudiencePackageList() {
|
|
|
- String dt = demandVideoMapperExt.getMaxDt(CHANNEL_NAME_XCX);
|
|
|
|
|
|
|
+ String dt = demandVideoMapperExt.getMaxDt(DemandChannelEnum.XCX.getValue());
|
|
|
if (!StringUtils.hasText(dt)) {
|
|
if (!StringUtils.hasText(dt)) {
|
|
|
return Collections.emptyList();
|
|
return Collections.emptyList();
|
|
|
}
|
|
}
|
|
|
- return demandVideoMapperExt.selectDistinctCrowdPackages(dt, CHANNEL_NAME_XCX);
|
|
|
|
|
|
|
+ return demandVideoMapperExt.selectDistinctCrowdPackages(dt, DemandChannelEnum.XCX.getValue());
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private List<XcxPlanItemVO> buildXcxPlanItemVOList(List<ContentPlatformXcxPlan> planList) {
|
|
private List<XcxPlanItemVO> buildXcxPlanItemVOList(List<ContentPlatformXcxPlan> planList) {
|