|
|
@@ -0,0 +1,258 @@
|
|
|
+package com.tzld.piaoquan.api.job;
|
|
|
+
|
|
|
+import com.alibaba.fastjson.JSONObject;
|
|
|
+import com.aliyun.odps.data.Record;
|
|
|
+import com.ctrip.framework.apollo.spring.annotation.ApolloJsonValue;
|
|
|
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
|
|
+import com.tzld.piaoquan.api.component.AdApiService;
|
|
|
+import com.tzld.piaoquan.api.component.AigcApiService;
|
|
|
+import com.tzld.piaoquan.api.component.DeepSeekApiService;
|
|
|
+import com.tzld.piaoquan.api.model.bo.AdPutCreativeComponentCostData;
|
|
|
+import com.tzld.piaoquan.api.model.bo.GoogleLLMResult;
|
|
|
+import com.tzld.piaoquan.api.model.bo.VideoDetail;
|
|
|
+import com.tzld.piaoquan.api.model.dto.AIResult;
|
|
|
+import com.tzld.piaoquan.api.model.vo.GhDetailVo;
|
|
|
+import com.tzld.piaoquan.api.service.GhDetailService;
|
|
|
+import com.tzld.piaoquan.api.util.AliOssFileTool;
|
|
|
+import com.tzld.piaoquan.api.util.CdnUtil;
|
|
|
+import com.tzld.piaoquan.growth.common.model.po.GhDetail;
|
|
|
+import com.tzld.piaoquan.growth.common.utils.DateUtil;
|
|
|
+import com.tzld.piaoquan.growth.common.utils.OdpsUtil;
|
|
|
+import com.tzld.piaoquan.growth.common.utils.TitleSimilarCheckUtil;
|
|
|
+import com.xxl.job.core.biz.model.ReturnT;
|
|
|
+import com.xxl.job.core.handler.annotation.XxlJob;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.apache.commons.collections.CollectionUtils;
|
|
|
+import org.apache.commons.lang3.StringUtils;
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
+import org.springframework.beans.factory.annotation.Value;
|
|
|
+import org.springframework.stereotype.Component;
|
|
|
+
|
|
|
+import java.util.*;
|
|
|
+import java.util.concurrent.*;
|
|
|
+import java.util.stream.Collectors;
|
|
|
+
|
|
|
+@Slf4j
|
|
|
+@Component
|
|
|
+public class GzhReplyVideoRefreshJob {
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ GhDetailService ghDetailService;
|
|
|
+ @Autowired
|
|
|
+ AdApiService adApiService;
|
|
|
+ @Autowired
|
|
|
+ AigcApiService aigcApiService;
|
|
|
+ @Autowired
|
|
|
+ DeepSeekApiService deepSeekApiService;
|
|
|
+
|
|
|
+ @ApolloJsonValue("${gzh.reply.video.refresh.job.wxIds:[]}")
|
|
|
+ private List<String> wxIds;
|
|
|
+ @Value("${gzh.reply.video.refresh.job.topNum:2}")
|
|
|
+ private Integer topNum;
|
|
|
+
|
|
|
+ private final static ExecutorService pool = new ThreadPoolExecutor(5, 5, 0L, TimeUnit.SECONDS,
|
|
|
+ new LinkedBlockingQueue<>(1000),
|
|
|
+ new ThreadFactoryBuilder().setNameFormat("GzhReplyVideoRefreshJob-%d").build(),
|
|
|
+ new ThreadPoolExecutor.AbortPolicy());
|
|
|
+
|
|
|
+ @XxlJob("gzhReplyVideoRefreshJob")
|
|
|
+ public ReturnT<String> gzhReplyVideoRefreshJob(String param) {
|
|
|
+ String dt =DateUtil.getBeforeDayDateString("yyyy-MM-dd");
|
|
|
+ if (StringUtils.isNotEmpty(param)) {
|
|
|
+ dt = param;
|
|
|
+ }
|
|
|
+ List<AdPutCreativeComponentCostData> list = adApiService.getCreativeComponentsCost(dt);
|
|
|
+ if (CollectionUtils.isEmpty(list)) {
|
|
|
+ return ReturnT.SUCCESS;
|
|
|
+ }
|
|
|
+ // 按公众号分组
|
|
|
+ Map<String, List<AdPutCreativeComponentCostData>> costDataMap = list.stream()
|
|
|
+ .filter(o ->CollectionUtils.isNotEmpty(o.getComponentIds()))
|
|
|
+ .collect(Collectors.groupingBy(AdPutCreativeComponentCostData::getWxId));
|
|
|
+ CountDownLatch cdl = new CountDownLatch(costDataMap.size());
|
|
|
+ // 获取所有投流账号信息
|
|
|
+ String accountMapSql = "SELECT account_id, account_name from loghubods.feishu_wechat_mp_account_base;";
|
|
|
+ List<Record> accountList = OdpsUtil.getOdpsData(accountMapSql);
|
|
|
+ Map<String, String> accountMap = new HashMap<>();
|
|
|
+ if (CollectionUtils.isNotEmpty(accountList)) {
|
|
|
+ for (Record record : accountList) {
|
|
|
+ String accountId = record.getString(0);
|
|
|
+ String accountName = record.getString(1);
|
|
|
+ accountMap.put(accountId, accountName);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 获取公众号信息
|
|
|
+ List<String> nameList = new ArrayList<>(accountMap.values());
|
|
|
+ List<GhDetail> ghDetailList = ghDetailService.getGhdetailByNames(nameList);
|
|
|
+ Map<String, GhDetail> ghDetailMap = ghDetailList.stream().collect(Collectors.toMap(GhDetail::getGhName, ghDetail -> ghDetail));
|
|
|
+ for (Map.Entry<String, List<AdPutCreativeComponentCostData>> entry : costDataMap.entrySet()) {
|
|
|
+ pool.submit(() -> {
|
|
|
+ try {
|
|
|
+ replaceGZHReplyVideo(entry.getKey(), entry.getValue(), ghDetailMap, accountMap);
|
|
|
+ } finally {
|
|
|
+ cdl.countDown();
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ cdl.await();
|
|
|
+ } catch (InterruptedException e) {
|
|
|
+ log.error("GzhReplyVideoRefreshJob error", e);
|
|
|
+ }
|
|
|
+ return ReturnT.SUCCESS;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void replaceGZHReplyVideo(String wxId, List<AdPutCreativeComponentCostData> costDataList,
|
|
|
+ Map<String, GhDetail> ghDetailMap,
|
|
|
+ Map<String, String> accountMap) {
|
|
|
+ if (!wxIds.contains(wxId)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ String accountName = accountMap.get(wxId);
|
|
|
+ if (StringUtils.isEmpty(accountName)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ GhDetail ghDetail = ghDetailMap.get(accountName);
|
|
|
+ if (ghDetail == null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ Map<String, Long> creativeIdTextMap = analysisImageText(costDataList);
|
|
|
+ log.info("GzhReplyVideoRefreshJob creativeIdTextMap:{}", JSONObject.toJSONString(creativeIdTextMap));
|
|
|
+ // 按cost排序 获取top2内容
|
|
|
+ List<String> sortedList = creativeIdTextMap.entrySet().stream()
|
|
|
+ .sorted(Map.Entry.<String, Long>comparingByValue().reversed())
|
|
|
+ .limit(topNum)
|
|
|
+ .map(Map.Entry::getKey)
|
|
|
+ .collect(Collectors.toList());
|
|
|
+ List<VideoDetail> searchVideos = new ArrayList<>();
|
|
|
+ for (String text : sortedList) {
|
|
|
+ // 提取关键词
|
|
|
+ String keywordPrompt =
|
|
|
+ "你是一位精通算法推荐逻辑的世界级短视频SEO专家。你擅长从标题中提炼出搜索量最大、用户意图最明确的“核心流量词”。\n" +
|
|
|
+ "\n" +
|
|
|
+ "# Task\n" +
|
|
|
+ "分析用户提供的视频标题,提炼出 2 个核心搜索关键词。\n" +
|
|
|
+ "\n" +
|
|
|
+ "# Constraints (必须严格遵守)\n" +
|
|
|
+ "1. **字数限制**:每个关键词严格控制在 **3个汉字以内**(包含3个字)。\n" +
|
|
|
+ "2. **选词逻辑**:\n" +
|
|
|
+ " - 优先提取核心名词(人名/物名)或高频动词。\n" +
|
|
|
+ " - 剔除虚词(如“的”、“了”、“吗”)。\n" +
|
|
|
+ " - 必须与原标题强相关,能覆盖用户搜索意图。\n" +
|
|
|
+ "3. **输出格式**:仅输出JSON数组,**严禁**包含任何解释、Markdown标记或其他文本。\n" +
|
|
|
+ "\n" +
|
|
|
+ "# Example\n" +
|
|
|
+ "输入:新手如何快速学会剪映剪辑\n" +
|
|
|
+ "输出:[\"剪映\",\"剪辑\"]\n" +
|
|
|
+ "\n" +
|
|
|
+ "输入:宝宝感冒流鼻涕怎么办\n" +
|
|
|
+ "输出:[\"感冒\",\"流鼻涕\"]\n" +
|
|
|
+ "\n" +
|
|
|
+ "# Input\n" +
|
|
|
+ "内容是: {{text}} \n" +
|
|
|
+ "\n" +
|
|
|
+ "# Output\n" +
|
|
|
+ "请基于上述规则,输出最终的JSON:";
|
|
|
+ keywordPrompt = keywordPrompt.replace("text", text);
|
|
|
+ AIResult aiResult = deepSeekApiService.requestOfficialApi(keywordPrompt, null, null, false);
|
|
|
+ if (aiResult.isSuccess()) {
|
|
|
+ List<String> keywords = JSONObject.parseArray(aiResult.getResponse().getChoices().get(0).getMessage().getContent(), String.class);
|
|
|
+ log.info("GzhReplyVideoRefreshJob text:{} keywords:{}", text, keywords);
|
|
|
+ VideoDetail videoDetail = searchVideoByKeyword(keywords, searchVideos);
|
|
|
+ if (videoDetail != null) {
|
|
|
+ searchVideos.add(videoDetail);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (searchVideos.size() == topNum) {
|
|
|
+ // 更新视频
|
|
|
+ updateVideoReply(ghDetail, searchVideos);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<String, Long> analysisImageText(List<AdPutCreativeComponentCostData> costDataList) {
|
|
|
+ Map<String, Long> creativeIdTextMap = new HashMap<>();
|
|
|
+ for (AdPutCreativeComponentCostData costData : costDataList) {
|
|
|
+ for (AdPutCreativeComponentCostData.AdPutTencentComponent component : costData.getComponentList()) {
|
|
|
+ String imageUrl = component.getImageUrl();
|
|
|
+ // download & upload to oss
|
|
|
+ String fileName = String.format("growth/image/%s_%d.jpg", costData.getCreativeId(), System.currentTimeMillis());
|
|
|
+ String ossImageUrl = AliOssFileTool.downloadAndSaveInOSS(fileName, imageUrl, "image/jpeg");
|
|
|
+ // 调用gemini-3-pro模型提取图片中的文字
|
|
|
+ String prompt = "识别图片里的文字,直接输出图片里的文字标题,但是不要输出图片里右下角的字,比如“点开看看”“看这里”等,仅输出标题,不要有多余的文字输出";
|
|
|
+ GoogleLLMResult result = aigcApiService.gemini("gemini-2.0-flash", prompt, Arrays.asList(ossImageUrl));
|
|
|
+ String text = result.getResult();
|
|
|
+ for (String existText : creativeIdTextMap.keySet()) {
|
|
|
+ if (TitleSimilarCheckUtil.isSimilar(text, existText, TitleSimilarCheckUtil.SIMILARITY_THRESHOLD)) {
|
|
|
+ text = existText;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ Long cost = creativeIdTextMap.getOrDefault(text, 0L);
|
|
|
+ cost += costData.getCost();
|
|
|
+ creativeIdTextMap.put(text, cost);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return creativeIdTextMap;
|
|
|
+ }
|
|
|
+
|
|
|
+ private VideoDetail searchVideoByKeyword(List<String> keywords, List<VideoDetail> searchVideos) {
|
|
|
+ if (CollectionUtils.isNotEmpty(keywords)) {
|
|
|
+ String searchVideoSql = "SELECT v.id, v.title, v.cover_img_path\n" +
|
|
|
+ "FROM videoods.wx_video v\n" +
|
|
|
+ "join videoods.wx_video_status vs on v.id = vs.video_id\n" +
|
|
|
+ "where (v.title like '%" + String.join("%' or v.title like '%", keywords) + "%') and v.status = 1 and vs.audit_status = 5\n" +
|
|
|
+ "order by v.play_count_total desc limit 10;";
|
|
|
+ List<Record> videoList = OdpsUtil.getOdpsData(searchVideoSql);
|
|
|
+ if (CollectionUtils.isNotEmpty(videoList)) {
|
|
|
+ for (Record record : videoList) {
|
|
|
+ Long videoId = Long.parseLong(record.getString(0));
|
|
|
+ String title = record.getString(1);
|
|
|
+ // 过滤重复视频
|
|
|
+ boolean filter = false;
|
|
|
+ for (VideoDetail searchVideo : searchVideos) {
|
|
|
+ if (searchVideo.getId().equals(videoId) || TitleSimilarCheckUtil.isSimilar(title, searchVideo.getTitle(), TitleSimilarCheckUtil.SIMILARITY_THRESHOLD)) {
|
|
|
+ filter = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (filter) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ String cover = record.getString(2);
|
|
|
+ VideoDetail videoDetail = new VideoDetail();
|
|
|
+ videoDetail.setId(videoId);
|
|
|
+ videoDetail.setCover(CdnUtil.getOssHttpUrl(cover));
|
|
|
+ videoDetail.setTitle(title);
|
|
|
+ return videoDetail;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void updateVideoReply(GhDetail ghDetail, List<VideoDetail> searchVideos) {
|
|
|
+ GhDetailVo ghDetailVo = new GhDetailVo();
|
|
|
+ ghDetailVo.setId(ghDetail.getId());
|
|
|
+ ghDetailVo.setAccountId(ghDetail.getGhId());
|
|
|
+ ghDetailVo.setAccountName(ghDetail.getGhName());
|
|
|
+ ghDetailVo.setCategory1(ghDetail.getCategory1());
|
|
|
+ ghDetailVo.setStrategyStatus(ghDetail.getStrategyStatus());
|
|
|
+ ghDetailVo.setType(ghDetail.getType());
|
|
|
+ ghDetailVo.setAutoreplySendMinigramNum(topNum);
|
|
|
+ ghDetailVo.setVideoIds(searchVideos.stream().map(VideoDetail::getId).collect(Collectors.toList()));
|
|
|
+ List<GhDetailVo.VideoDetail> videoDetailList = new ArrayList<>();
|
|
|
+ for (int i = 0; i < searchVideos.size(); i++) {
|
|
|
+ VideoDetail videoDetail = searchVideos.get(i);
|
|
|
+ GhDetailVo.VideoDetail videoDetailVo = new GhDetailVo.VideoDetail();
|
|
|
+ videoDetailVo.setVideoId(videoDetail.getId());
|
|
|
+ videoDetailVo.setSort(i + 1);
|
|
|
+ videoDetailVo.setTitle(videoDetail.getTitle());
|
|
|
+ videoDetailVo.setCover(videoDetail.getCover());
|
|
|
+ videoDetailList.add(videoDetailVo);
|
|
|
+ }
|
|
|
+ ghDetailVo.setVideoList(videoDetailList);
|
|
|
+ ghDetailService.updateDetail(ghDetailVo);
|
|
|
+ }
|
|
|
+}
|