|
|
@@ -631,6 +631,10 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
|
|
|
private static final String SOURCE_PRIOR = "prior";
|
|
|
private static final String SOURCE_POSTERIOR = "posterior";
|
|
|
private static final String SOURCE_HOT = "hot";
|
|
|
+ private static final String SOURCE_SELECTED = "selected";
|
|
|
+ /** 单用户历史已发布 video_id 上限:超过 500 条的老视频不再参与 NOT IN 过滤,
|
|
|
+ * 控制 SQL 体积,老视频本来曝光机会就低,丢失精度可接受。 */
|
|
|
+ private static final int EXCLUDE_PUBLISHED_LIMIT = 500;
|
|
|
|
|
|
/**
|
|
|
* 推导 channel_name(人群_渠道) 作为 demand 池强过滤。
|
|
|
@@ -655,11 +659,23 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
|
|
|
@Override
|
|
|
public Page<VideoContentItemVO> getVideoContentList(VideoContentListParam param) {
|
|
|
ContentPlatformAccount user = LoginUserContext.getUser();
|
|
|
+ String source = param.getSource();
|
|
|
+ // 历史已选自带 title 模糊搜索,优先于通用 title 向量搜索分发
|
|
|
+ if (SOURCE_SELECTED.equalsIgnoreCase(source)) {
|
|
|
+ return getSelectedSourcePaged(param, user);
|
|
|
+ }
|
|
|
+ // 其他 source(含 title 向量搜索路径不参与):预查询当前账号历史已发布 video_id,
|
|
|
+ // 用于 hot/prior/posterior 池排除,避免重复曝光。
|
|
|
+ if (user != null && user.getId() != null && !StringUtils.hasText(param.getTitle())) {
|
|
|
+ List<Long> excludeVideoIds = getUserPublishedVideoIds(user.getId(), param.getType());
|
|
|
+ if (CollectionUtils.isNotEmpty(excludeVideoIds)) {
|
|
|
+ param.setExcludeVideoIds(excludeVideoIds);
|
|
|
+ }
|
|
|
+ }
|
|
|
// 如果 title 有内容,调用 manager 平台接口搜索
|
|
|
if (StringUtils.hasText(param.getTitle())) {
|
|
|
return getVideoContentListByTitleV2(param);
|
|
|
}
|
|
|
- String source = param.getSource();
|
|
|
if (SOURCE_PRIOR.equalsIgnoreCase(source)) {
|
|
|
return getSingleSourcePage(param, user, SOURCE_PRIOR);
|
|
|
}
|
|
|
@@ -672,6 +688,114 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
|
|
|
return getInterleavedPage(param, user);
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 历史已选:返回当前登录账号在当前入口/type 下已经发布过的视频(按 video_id 去重,取最近一次).
|
|
|
+ * 按入口隔离 — 企微入口查 qw_plan_video,公众号入口查 gzh_plan_video.
|
|
|
+ * 排序:MAX(create_timestamp) DESC(最近发布在前).
|
|
|
+ * type 未指定/不在已知范围 → 返回空,前端引导用户先选行业.
|
|
|
+ */
|
|
|
+ private Page<VideoContentItemVO> getSelectedSourcePaged(VideoContentListParam param, ContentPlatformAccount user) {
|
|
|
+ int pageSize = param.getPageSize();
|
|
|
+ int pageNum = param.getPageNum();
|
|
|
+ Page<VideoContentItemVO> result = new Page<>(pageNum, pageSize);
|
|
|
+ if (user == null || user.getId() == null) {
|
|
|
+ result.setTotalSize(0);
|
|
|
+ result.setObjs(new ArrayList<>());
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+ Integer type = param.getType();
|
|
|
+ Long loginAccountId = user.getId();
|
|
|
+ String title = StringUtils.hasText(param.getTitle()) ? param.getTitle() : null;
|
|
|
+ int offset = (pageNum - 1) * pageSize;
|
|
|
+
|
|
|
+ Integer qwPlanType = mapToQwPlanType(type);
|
|
|
+ Integer gzhPlanType = mapToGzhPlanType(type);
|
|
|
+
|
|
|
+ int count;
|
|
|
+ List<VideoContentItemVO> list;
|
|
|
+ if (qwPlanType != null) {
|
|
|
+ count = nullToZero(planMapperExt.getSelectedQwVideoCount(loginAccountId, qwPlanType, title));
|
|
|
+ result.setTotalSize(count);
|
|
|
+ if (count == 0 || offset >= count) {
|
|
|
+ result.setObjs(new ArrayList<>());
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+ list = planMapperExt.getSelectedQwVideoList(loginAccountId, qwPlanType, title, offset, pageSize);
|
|
|
+ } else if (gzhPlanType != null) {
|
|
|
+ count = nullToZero(planMapperExt.getSelectedGzhVideoCount(loginAccountId, gzhPlanType, title));
|
|
|
+ result.setTotalSize(count);
|
|
|
+ if (count == 0 || offset >= count) {
|
|
|
+ result.setObjs(new ArrayList<>());
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+ list = planMapperExt.getSelectedGzhVideoList(loginAccountId, gzhPlanType, title, offset, pageSize);
|
|
|
+ } else {
|
|
|
+ // type 缺失或未知:历史已选不知道走哪张表,直接返回空
|
|
|
+ result.setTotalSize(0);
|
|
|
+ result.setObjs(new ArrayList<>());
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+ if (list == null) {
|
|
|
+ list = new ArrayList<>();
|
|
|
+ }
|
|
|
+ for (VideoContentItemVO v : list) {
|
|
|
+ v.setSource(SOURCE_SELECTED);
|
|
|
+ }
|
|
|
+ result.setObjs(list);
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 取当前登录账号在指定 type 入口下已发布的 video_id(最多 EXCLUDE_PUBLISHED_LIMIT 条,按发布时间倒序).
|
|
|
+ * type 无法映射时返回空,等同于不过滤.
|
|
|
+ */
|
|
|
+ private List<Long> getUserPublishedVideoIds(Long loginAccountId, Integer type) {
|
|
|
+ Integer qwPlanType = mapToQwPlanType(type);
|
|
|
+ if (qwPlanType != null) {
|
|
|
+ return planMapperExt.getUserPublishedQwVideoIds(loginAccountId, qwPlanType, EXCLUDE_PUBLISHED_LIMIT);
|
|
|
+ }
|
|
|
+ Integer gzhPlanType = mapToGzhPlanType(type);
|
|
|
+ if (gzhPlanType != null) {
|
|
|
+ return planMapperExt.getUserPublishedGzhVideoIds(loginAccountId, gzhPlanType, EXCLUDE_PUBLISHED_LIMIT);
|
|
|
+ }
|
|
|
+ return Collections.emptyList();
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 前端 param.type → qw_plan.type;非企微入口返回 null. */
|
|
|
+ private Integer mapToQwPlanType(Integer type) {
|
|
|
+ if (type == null) return null;
|
|
|
+ if (type == 2) return QwPlanTypeEnum.GROUP.getVal();
|
|
|
+ if (type == 3) return QwPlanTypeEnum.REPLY.getVal();
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 前端 param.type → gzh_plan.type;非公众号入口返回 null. */
|
|
|
+ private Integer mapToGzhPlanType(Integer type) {
|
|
|
+ if (type == null) return null;
|
|
|
+ if (type == 0) return ContentPlatformGzhPlanTypeEnum.AUTO_REPLY.getVal();
|
|
|
+ if (type == 1) return ContentPlatformGzhPlanTypeEnum.FWH_PUSH.getVal();
|
|
|
+ if (type == 4) return ContentPlatformGzhPlanTypeEnum.GZH_PUSH.getVal();
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private int nullToZero(Integer v) {
|
|
|
+ return v == null ? 0 : v;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 内存过滤 demand 池行,剔除当前用户已发布的 video_id.
|
|
|
+ * 在 demand pipeline 早期调用,避免被剔除的视频占用 groupAndTopK 名额.
|
|
|
+ */
|
|
|
+ private List<ContentPlatformDemandVideo> excludePublishedRows(List<ContentPlatformDemandVideo> rows, List<Long> excludeVideoIds) {
|
|
|
+ if (CollectionUtils.isEmpty(rows) || CollectionUtils.isEmpty(excludeVideoIds)) {
|
|
|
+ return rows;
|
|
|
+ }
|
|
|
+ Set<Long> excludeSet = new HashSet<>(excludeVideoIds);
|
|
|
+ return rows.stream()
|
|
|
+ .filter(r -> r.getVideoId() == null || !excludeSet.contains(r.getVideoId()))
|
|
|
+ .collect(Collectors.toList());
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 单一来源分页:与穿插使用同一套候选构建逻辑(人群需求/优质相似各 2 阶段、组内 score top K),
|
|
|
* 再按 pageNum/pageSize 在内存中分页。totalSize = 去重后总数。
|
|
|
@@ -884,6 +1008,8 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
|
|
|
rows = demandVideoMapperExt.selectForRecommend(
|
|
|
dt, channelName, null, DEMAND_STRATEGY_PRIOR_SCENE, null, null, null, null, null, category, limit, false);
|
|
|
}
|
|
|
+ // 0. 历史已选剔除:用户已发布过的 video_id 不再出现在 priorScene 池中
|
|
|
+ rows = excludePublishedRows(rows, param.getExcludeVideoIds());
|
|
|
// 1. 同 video_id 取 total_rov 最大的代表行(SQL 已排序,putIfAbsent 保留首次)
|
|
|
LinkedHashMap<Long, ContentPlatformDemandVideo> bestPerVideo = new LinkedHashMap<>();
|
|
|
for (ContentPlatformDemandVideo r : rows) {
|
|
|
@@ -942,6 +1068,9 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
|
|
|
dt, channelName, null, DEMAND_STRATEGY_PRIOR, PRIOR_PREMIUM_DIMENSION, null, null, null, null, category, fetchLimit, false);
|
|
|
}
|
|
|
|
|
|
+ // 历史已选剔除:在 rov 过滤和分组裁剪前剔除,避免被排除的视频占用 topK 名额
|
|
|
+ rows = excludePublishedRows(rows, param.getExcludeVideoIds());
|
|
|
+
|
|
|
// 近 7 日 rov 下限,过滤掉低质量近期表现的视频(0513 验证 ≥0.02 保留 ~41%)
|
|
|
rows = rows.stream()
|
|
|
.filter(r -> r.getRov() != null && r.getRov() >= DEMAND_MIN_ROV)
|
|
|
@@ -1024,6 +1153,9 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
|
|
|
dt, channelName, null, DEMAND_STRATEGY_POSTERIOR, null, null, null, null, null, category, fetchLimit, true);
|
|
|
}
|
|
|
|
|
|
+ // 历史已选剔除:在 rov 过滤和分组裁剪前剔除,避免被排除的视频占用 topK 名额
|
|
|
+ rows = excludePublishedRows(rows, param.getExcludeVideoIds());
|
|
|
+
|
|
|
// 近 7 日 rov 下限,与 prior 池一致(cdjh 0514 验证 ≥0.02 保留 ~54%)
|
|
|
rows = rows.stream()
|
|
|
.filter(r -> r.getRov() != null && r.getRov() >= DEMAND_MIN_ROV)
|