|
|
@@ -646,8 +646,16 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
|
|
|
/** 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));
|
|
|
+ /** 搜索向量召回 topN:取较大值,确保与 demand 白名单交集后仍有足量结果。 */
|
|
|
+ private static final int SEARCH_VECTOR_TOP_N = 500;
|
|
|
/**
|
|
|
* 合作类渠道:demand.crowd_segment 语义=合作方代码(yy/szhx/cdjh/...),与登录账号 ContentPlatformAccount.channel 同义,可作为过滤条件。
|
|
|
* 其余渠道(投流-稳定/服务号投流):demand.crowd_segment 语义=人群分组标签(R50*xx/回流xx),与登录账号合作方无关,绝不可用 user.channel 过滤。
|
|
|
@@ -706,9 +714,9 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
|
|
|
@Override
|
|
|
public Page<VideoContentItemVO> getVideoContentList(VideoContentListParam param) {
|
|
|
ContentPlatformAccount user = LoginUserContext.getUser();
|
|
|
- // 如果 title 有内容,调用 manager 平台接口搜索
|
|
|
+ // title 非空 → 走 demand 池白名单 + 向量召回交集搜索(切掉管理平台关键词 fallback,只对 type∈{2,3} + 投流类渠道放开)
|
|
|
if (StringUtils.hasText(param.getTitle())) {
|
|
|
- return getVideoContentListByTitleV2(param);
|
|
|
+ return searchByTitleInDemandPool(param, user);
|
|
|
}
|
|
|
String source = param.getSource();
|
|
|
if (SOURCE_PRIOR.equalsIgnoreCase(source)) {
|
|
|
@@ -2016,6 +2024,97 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
|
|
|
return result;
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 搜索:在当前入口的 demand 白名单(prior+posterior 池,rov>=0.02)内对向量召回结果做交集 + 排序。
|
|
|
+ * 门控:
|
|
|
+ * - 用户身份 type ∈ {2 自营, 3 代理},否则返回空
|
|
|
+ * - 渠道 ∈ {小程序投流-稳定, 公众号投流-稳定},否则返回空
|
|
|
+ * - 小程序投流必填 crowdPackage,公众号投流必填 ghName,否则返回空
|
|
|
+ * 流程:
|
|
|
+ * 1. demand 白名单(channel + crowdSegment/channelLevel3 + demand_strategy IN (人群需求,优质相似) + rov>=0.02)
|
|
|
+ * 2. 同 video_id 取 sceneSumRov 最大的代表行
|
|
|
+ * 3. 向量召回 topN=500
|
|
|
+ * 4. 交集:仅保留向量召回中出现在白名单的 video_id
|
|
|
+ * 5. 排序:向量 score DESC,缺失/相同时 sceneSumRov DESC
|
|
|
+ * 6. 一次性全量返回(不分页;白名单+召回交集量级小)
|
|
|
+ */
|
|
|
+ private Page<VideoContentItemVO> searchByTitleInDemandPool(VideoContentListParam param, ContentPlatformAccount user) {
|
|
|
+ Page<VideoContentItemVO> empty = new Page<>(param.getPageNum(), param.getPageSize());
|
|
|
+ empty.setTotalSize(0);
|
|
|
+ empty.setObjs(new ArrayList<>());
|
|
|
+
|
|
|
+ if (user == null || !USER_TYPES_ALLOW_SEARCH.contains(user.getType())) {
|
|
|
+ return empty;
|
|
|
+ }
|
|
|
+ String channelName = resolveChannelName(param);
|
|
|
+ if (channelName == null || !CHANNELS_ALLOW_SEARCH.contains(channelName)) {
|
|
|
+ return empty;
|
|
|
+ }
|
|
|
+ boolean isXcx = CHANNEL_NAME_XCX.equals(channelName);
|
|
|
+ String crowdPackage = StringUtils.hasText(param.getCrowdPackage()) ? param.getCrowdPackage() : null;
|
|
|
+ String ghName = StringUtils.hasText(param.getGhName()) ? param.getGhName() : null;
|
|
|
+ if (isXcx && crowdPackage == null) return empty;
|
|
|
+ if (!isXcx && ghName == null) return empty;
|
|
|
+
|
|
|
+ String dt = demandVideoMapperExt.getMaxDt(channelName);
|
|
|
+ if (!StringUtils.hasText(dt)) return empty;
|
|
|
+
|
|
|
+ List<ContentPlatformDemandVideo> whitelist = demandVideoMapperExt.selectSearchWhitelist(
|
|
|
+ dt, channelName, isXcx ? crowdPackage : null, isXcx ? null : ghName);
|
|
|
+ if (CollectionUtils.isEmpty(whitelist)) return empty;
|
|
|
+
|
|
|
+ // video_id 去重:取 sceneSumRov 最大的代表行
|
|
|
+ Map<Long, ContentPlatformDemandVideo> bestPerVideo = new HashMap<>();
|
|
|
+ for (ContentPlatformDemandVideo r : whitelist) {
|
|
|
+ if (r.getVideoId() == null) continue;
|
|
|
+ ContentPlatformDemandVideo cur = bestPerVideo.get(r.getVideoId());
|
|
|
+ double rSum = r.getSceneSumRov() == null ? 0d : r.getSceneSumRov();
|
|
|
+ double curSum = cur == null ? -1d : (cur.getSceneSumRov() == null ? 0d : cur.getSceneSumRov());
|
|
|
+ if (cur == null || rSum > curSum) {
|
|
|
+ bestPerVideo.put(r.getVideoId(), r);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ JSONObject vectorData = managerApiService.recallVideoWithScore(param.getTitle(), SEARCH_VECTOR_TOP_N);
|
|
|
+ if (vectorData == null) return empty;
|
|
|
+ JSONArray items = vectorData.getJSONArray("items");
|
|
|
+ if (items == null || items.isEmpty()) return empty;
|
|
|
+
|
|
|
+ // 交集:按向量召回顺序遍历,留下 whitelist 命中的 video_id;同 video_id 去重
|
|
|
+ List<VideoContentItemVO> hits = new ArrayList<>();
|
|
|
+ Set<Long> seen = new HashSet<>();
|
|
|
+ for (int i = 0; i < items.size(); i++) {
|
|
|
+ JSONObject item = items.getJSONObject(i);
|
|
|
+ Long videoId = item.getLong("videoId");
|
|
|
+ if (videoId == null || !bestPerVideo.containsKey(videoId) || !seen.add(videoId)) continue;
|
|
|
+ ContentPlatformDemandVideo demand = bestPerVideo.get(videoId);
|
|
|
+ VideoContentItemVO vo = buildDemandVideoContentItemVOList(Collections.singletonList(demand)).get(0);
|
|
|
+ vo.setSearchSource("vector");
|
|
|
+ vo.setScore(item.getDouble("score")); // 用向量 score 覆盖 demand.score,主排序键
|
|
|
+ hits.add(vo);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 排序:向量 score DESC,缺失/相同时 sceneSumRov DESC
|
|
|
+ hits.sort((a, b) -> {
|
|
|
+ double sa = a.getScore() == null ? 0d : a.getScore();
|
|
|
+ double sb = b.getScore() == null ? 0d : b.getScore();
|
|
|
+ int c = Double.compare(sb, sa);
|
|
|
+ if (c != 0) return c;
|
|
|
+ double ra = a.getSceneSumRov() == null ? 0d : a.getSceneSumRov();
|
|
|
+ double rb = b.getSceneSumRov() == null ? 0d : b.getSceneSumRov();
|
|
|
+ return Double.compare(rb, ra);
|
|
|
+ });
|
|
|
+
|
|
|
+ Page<VideoContentItemVO> page = new Page<>(param.getPageNum(), param.getPageSize());
|
|
|
+ page.setTotalSize(hits.size());
|
|
|
+ // 全量在后端计算好,按 pageNum/pageSize 切片(单次召回,翻页不再调向量服务)
|
|
|
+ int pageSize = param.getPageSize() > 0 ? param.getPageSize() : 10;
|
|
|
+ int from = Math.min((param.getPageNum() - 1) * pageSize, hits.size());
|
|
|
+ int to = Math.min(param.getPageNum() * pageSize, hits.size());
|
|
|
+ page.setObjs(new ArrayList<>(hits.subList(from, to)));
|
|
|
+ return page;
|
|
|
+ }
|
|
|
+
|
|
|
private List<VideoContentItemVO> buildDemandVideoContentItemVOList(List<ContentPlatformDemandVideo> videoList) {
|
|
|
if (CollectionUtils.isEmpty(videoList)) {
|
|
|
return new ArrayList<>();
|