videoContentList 推荐列表排序逻辑接口:
POST /contentPlatform/plan/videoContentList入口:ContentPlatformPlanServiceImpl.getVideoContentList数据源:content_platform_demand_video(人群需求/优质相似/人群需求-场景)+content_platform_video(全局热门)数据组重命名:旧值「先验需求/后验需求/先验需求-场景」→ 新值「人群需求/优质相似/人群需求-场景」(2026-05 改名,本文档已对齐新值)。
适用:当前分支
cooperation_video_candidate_pool_improved_lld_0509(含 commit2860bdce)。
title 非空 ───────────► 全站搜索 (getVideoContentListByTitle)
source=prior ───────────► 单源「粉丝喜欢」(getSingleSourcePage / prior)
source=posterior ───────► 单源「已发优质相似」(getSingleSourcePage / posterior)
source=hot ───────────► 单源「全局热门」(getHotSourcePaged)
source 空(默认) ──────► 四路随机穿插 (getInterleavedPage)
当前前端:
prior和posterior两个 tab 已 disabled(提示"功能正在完善中"),生产实际只走「全部」(四路穿插)和「全局热门」单源。
| 常量 | 值 | 含义 |
|---|---|---|
DEMAND_CANDIDATE_LIMIT |
10000 | 每个 demand 池最大候选条数 |
HOT_CANDIDATE_LIMIT |
10000 | hot 池候选条数 |
TOP_K_PER_DEMAND |
3 | demand 池组内取前 K |
DEMAND_STRATEGY_PRIOR |
"人群需求" |
人群需求池过滤值(prior,旧名「先验需求」) |
DEMAND_STRATEGY_PRIOR_SCENE |
"人群需求-场景" |
场景池过滤值(旧名「先验需求-场景」) |
DEMAND_STRATEGY_POSTERIOR |
"优质相似" |
优质相似池过滤值(posterior,旧名「后验需求」) |
PRIOR_PREMIUM_DIMENSION |
"传播的头部" |
prior 池 dimension 强过滤 |
PRIOR_MIN_ROV |
0.02 |
prior 池近 7 日 rov 下限(只对 prior 池;priorScene/posterior 仍 >0) |
CHANNEL_NAME_GZH |
"公众号合作-即转-稳定" |
公众号入口对应 channel_name 强过滤值 |
CHANNEL_NAME_QW |
"群/企微合作-稳定" |
企微入口对应 channel_name 强过滤值 |
PRIOR_GROUP_KEEP_RATIO |
0.5 |
prior 池"特征组"按 total_rov 分位保留比例 |
POSTERIOR_FILTER_ABS_LIKE |
"绝对高效率%" |
posterior 池 A 段 demand_filter_sort_strategy LIKE |
POSTERIOR_FILTER_REL_LIKE |
"相对裂变率%" |
posterior 池 B 段 demand_filter_sort_strategy LIKE |
POSTERIOR_DRIVE_DIMENSION_TIME |
"昨日" |
posterior 池强制 drive_dimension_time |
公共强过滤(所有 demand 池 SQL):dt = max(dt) AND status = 1 AND crowd_segment = user.channel AND channel_name = resolveChannelName(param)(可空)。
resolveChannelName(param) — 入口 → channel_name 映射为避免同一 crowd_segment 在公众号系与企微系都存在(已确认 gzyhc / wxm 双渠道并存)时,选企微入口却拉到公众号数据,3 个 demand 池 SQL 在源头加 channel_name 强过滤。映射依据 VideoContentListParam:
param.type |
含义 | 映射 channel_name |
|---|---|---|
0 自动回复 / 1 服务号推送 / 4 公众号推送 |
公众号系 | CHANNEL_NAME_GZH |
2 企微-社群 / 3 企微-自动回复 |
企微系 | CHANNEL_NAME_QW |
999 / 其它 |
不限平台 | 进入 fallback |
Fallback:type 空或不在上表时,若 param.ghName 非空(只有公众号入口会带 ghName)→ CHANNEL_NAME_GZH;否则返回 null(不加 channel_name 过滤,保留旧行为)。
设计动机:
crowd_segment是按客户(广告主)切的,而同一客户可能同时投公众号 + 企微;数据落库时按"人群_渠道"打channel_name。前端切换入口后type一定会变,所以让type当一级路由信号最稳。
fetchPriorSceneCandidates(场景池)目的:用户所属 channel 在"场景"维度命中的人群需求-场景行,按视频近 7 日表现(rov)排序。
SELECT ... FROM content_platform_demand_video
WHERE dt=:maxDt AND status=1
AND crowd_segment=:userChannel
AND channel_name=:resolvedChannelName -- 若 resolveChannelName 命中(公众号/企微)
AND demand_strategy='人群需求-场景'
AND channel_level3=:ghName -- 若传入
ORDER BY total_rov DESC, score DESC
LIMIT 10000
退化阶梯(细→粗):
ghName 非空但查 0 条 → 去掉 ghName 再查一次(拿全 channel 兜底)channel_name 非空 → 再去掉 crowd_segment,只按 channel_name 拉(跨渠道兜底,如公众号账号切到企微入口)channel_name 在所有退化层始终保留(强过滤,跨渠道污染必须先挡掉)后处理(顺序):
videoId 去重,保留首次出现(SQL 已按 total_rov DESC, score DESC 排序 → 首次 = 该视频的"最强代表需求"行)rov 为 null 或 ≤0 的视频rov DESC 主键,total_rov DESC 次级 tiebreaker输出顺序:视频按 rov(近 7 日表现)DESC;同 rov 时按代表需求的 total_rov DESC。
设计动机:场景需求里同一视频会对应多个特征点,但前端只下发一条;用 total_rov 选"最强代表需求",再以视频自身的 rov 决定整体排序,避免按需求维度排序导致同 dimension/标准要素的视频在列表里扎堆。
fetchPriorCandidates(人群需求池,prior)目的:人群需求里,只取 dimension='传播的头部' 维度,并按 channel 内"特征需求强度"分位裁掉弱题材。
单段查询:
SELECT ... WHERE ... AND channel_name=:resolvedChannelName(可空)
AND demand_strategy='人群需求' AND dimension='传播的头部' ...
ORDER BY total_rov DESC, score DESC LIMIT 30000
退化阶梯:ghName 非空且 0 条 → 去 ghName 重查;仍 0 且 channel_name 非空 → 再去 crowd_segment 重查(跨渠道兜底)。channel_name 始终保留。
[新] 特征组分位裁剪 (retainTopGroupsByTotalRov,keepRatio=0.5):
(point_type, standard_element) 分组,取每组 max(total_rov)(即该特征的人群需求强度)设计动机:
total_rov在 prior 池 = 群体对(point_type, standard_element)特征的需求强度。低 total_rov 说明群体不爱这个题材,把对应视频堆在列表底部没意义,直接剪掉。
进 groupAndTopK:
(point_type, standard_element)rov <= 0 或 null(近 7 日无表现)total_rov DESCscore DESC,组内 videoId 去重,每组最多 3 条最后按 limit=10000 截断。
最终顺序:保留 top 50% 特征组内,组按总 ROV,组内按 score。
fetchPosteriorCandidates(优质相似池,posterior)目的:优质相似里,"昨日"驱动的"绝对高效率"先出,再出"相对裂变率"。
A、B 两段独立查询:
-- A 段: demand_filter_sort_strategy LIKE '绝对高效率%'
SELECT ... WHERE ... AND channel_name=:resolvedChannelName(可空)
AND demand_strategy='优质相似'
AND demand_filter_sort_strategy LIKE '绝对高效率%'
AND drive_dimension_time='昨日'
AND (title IS NULL OR demand_content_title IS NULL OR title <> demand_content_title)
ORDER BY total_rov DESC, score DESC LIMIT 30000
-- B 段: demand_filter_sort_strategy LIKE '相对裂变率%'
SELECT ... AND demand_filter_sort_strategy LIKE '相对裂变率%' AND drive_dimension_time='昨日' ...
退化阶梯:A、B 都空且 ghName 非空 → 去 ghName 重查;A、B 仍都空且 channel_name 非空 → 再去 crowd_segment 重查(跨渠道兜底)。drive_dimension_time='昨日' 与 channel_name 强过滤始终保留。
每段进 groupAndTopK:
demand_content_idrov <= 0 或 nullexcludeSelfTitle=true → 用 TitleNormalizer.isSelfTitle 跳过自标题total_rov DESCscore DESC,去重,每组最多 3 条A 段 + B 段 顺序拼接 → 跨段 videoId 去重 → 截 10000。
最终顺序:A 段(绝对高效率) 在前;段内组按总 ROV,组内按 score。
fetchHotCandidates(热门池)复用现有 planMapperExt.getVideoList(...):
dt = videoMaxDt,datastatDt = videoDatastatMaxDtsort/type/channel/strategy 由请求和 param.getSort() 派生fission_rate DESC 或 score DESC)getInterleavedPage)priorScene → 标 source='prior'
prior → 标 source='prior'
posterior → 标 source='posterior'
hot → 标 source='hot'
priorScene 和 prior 对外都是
source='prior'(前端"粉丝喜欢"统一标签);浮层细节通过demandStrategy字段区分场景。
pointer[i] 和 exhausted[i],以及全局 emittedIds + emittedTitles。seed = userId ^ LocalDate.now().toString().hashCode()
shouldSkipForDedup)merged、记入 emittedIds 和 emittedTitlespaginateCandidates:totalSize = merged.size(),按 pageNum/pageSize 内存切片返回。标题去重用
TitleNormalizer.normalize(去 emoji/空白/全半角),应对运营把同段内容重复上传成多个 video_id(如🔴她走了,台湾再无洪秀柱!对应 67396144 / 67812469 两条)。 单源source=prior模式的interleavePriorWithScene也用同一套(video_id + 标题)去重。
priorScene / prior / posterior 内部相对顺序保留(场景按视频 rov;prior/posterior 按组 total_rov + 组内 score),随机只影响"哪一池先出"source=prior)scene = fetchPriorSceneCandidates(...)
prior = fetchPriorCandidates(...)
list = interleavePriorWithScene(scene, prior) // 严格 1:1
interleavePriorWithScene:
scene 取一条未发的 → 再从 prior 取一条未发的,严格 1:1 交替seen<videoId> 跨池去重,场景优先(先到先得)每条 VO 设 source='prior',然后 paginateCandidates 切片。
source=posterior)list = fetchPosteriorCandidates(...) // 顺序 = 绝对高效率段 → 相对裂变率段
每条 VO 设 source='posterior',paginateCandidates 切片。
source=hot)不走候选池:复用原 planMapperExt.getVideoCount + getVideoList 真分页链路,DB 端 LIMIT/OFFSET。VO source='hot'。
source 字段source |
含义 | 数据来源 | 浮层 demand 字段 |
|---|---|---|---|
prior |
粉丝喜欢(人群需求-场景 + 人群需求-头部) | content_platform_demand_video |
有完整字段,可看 demandStrategy 区分 |
posterior |
已发优质相似 | content_platform_demand_video |
有完整字段 |
hot |
全局热门 | content_platform_video |
只有基础字段(demand 相关字段为空) |
┌────────────────────────────────────────────────────────┐
│ 1. 拉 4 个候选池 (每个池都自己分阶段、分组、去重、排序) │
└────────────────────────────────────────────────────────┘
│
┌───────────────────────┬──┴──┬─────────────────────────────┐
▼ ▼ ▼ ▼
priorScene(10000) prior(10000) posterior(10000) hot(10000)
视频维度 rov DESC 传播头部 A: 绝对高效率 + 昨日 SQL 默认
(代表需求 total_rov 选最大) B: 相对裂变率 + 昨日 (sort 决定)
组(point_type, standard_element) top3
组(demand_content_id) top3
│
▼
┌────────────────────────────────────────────────────────┐
│ 2. 四路随机穿插 │
│ - 种子 = userId ^ 今天 │
│ - 跨池 videoId 去重 │
│ - 池内顺序保留 │
└────────────────────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────┐
│ 3. paginateCandidates │
│ totalSize = 全量, 按 pageNum/pageSize 切片 │
└────────────────────────────────────────────────────────┘
| 内容 | 位置 |
|---|---|
| 主入口路由 | ContentPlatformPlanServiceImpl.java:626 |
| 单源分页 | ContentPlatformPlanServiceImpl.java:649 |
| 单源 hot | ContentPlatformPlanServiceImpl.java:698 |
| 四路穿插 | ContentPlatformPlanServiceImpl.java:743 |
| 场景池 fetcher | ContentPlatformPlanServiceImpl.java:812 |
| 人群需求池 fetcher (prior) | ContentPlatformPlanServiceImpl.java:840 |
| 优质相似池 fetcher (posterior) | ContentPlatformPlanServiceImpl.java:875 |
groupAndTopK 通用排序 |
ContentPlatformPlanServiceImpl.java:912 |
| 段间拼接 + 去重 | ContentPlatformPlanServiceImpl.java:956 |
| 热门池 fetcher | ContentPlatformPlanServiceImpl.java:976 |
| Mapper SQL | mapper/contentplatform/ext/ContentPlatformDemandVideoMapperExt.xml (selectForRecommend) |