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 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 gzhReplyVideoRefreshJob(String param) { String dt =DateUtil.getBeforeDayDateString("yyyy-MM-dd"); if (StringUtils.isNotEmpty(param)) { dt = param; } List list = adApiService.getCreativeComponentsCost(dt); if (CollectionUtils.isEmpty(list)) { return ReturnT.SUCCESS; } // 按公众号分组 Map> 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 accountList = OdpsUtil.getOdpsData(accountMapSql); Map 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); } // todo 服务号临时处理,后续读取大数据表 accountMap.put("wxcd30ad28e2b7ebc3", "票圈精彩"); accountMap.put("wx3ec82b6a91dd0954", "美好时光记叙图书"); accountMap.put("wx154baf080cea7044", "银发暖伴生活志"); } // 获取公众号信息 List nameList = new ArrayList<>(accountMap.values()); List ghDetailList = ghDetailService.getGhdetailByNames(nameList); Map ghDetailMap = ghDetailList.stream().collect(Collectors.toMap(GhDetail::getGhName, ghDetail -> ghDetail)); for (Map.Entry> 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 costDataList, Map ghDetailMap, Map 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 creativeIdTextMap = analysisImageText(costDataList); log.info("GzhReplyVideoRefreshJob accountName:{} creativeIdTextMap:{}", accountName, JSONObject.toJSONString(creativeIdTextMap)); // 按cost排序 获取top2内容 List sortedList = creativeIdTextMap.entrySet().stream() .sorted((o1, o2) -> o2.getValue().getLong("cost").compareTo(o1.getValue().getLong("cost"))) .limit(topNum) .map(Map.Entry::getValue) .collect(Collectors.toList()); List searchVideos = new ArrayList<>(); List existTitles = new ArrayList<>(); for (JSONObject obj : sortedList) { String text = obj.getString("title"); // 提取关键词 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 keywords = JSONObject.parseArray(aiResult.getResponse().getChoices().get(0).getMessage().getContent(), String.class); log.info("GzhReplyVideoRefreshJob accountName:{} text:{} keywords:{}", accountName, text, keywords); VideoDetail videoDetail = searchVideoByKeyword(keywords, searchVideos, existTitles); if (videoDetail != null) { existTitles.add(videoDetail.getTitle()); existTitles.add(text); videoDetail.setTitle(text); videoDetail.setCover(obj.getString("coverImgUrl")); searchVideos.add(videoDetail); } } } if (searchVideos.size() == topNum) { // 更新视频 updateVideoReply(ghDetail, searchVideos); } } private Map analysisImageText(List costDataList) { Map creativeIdTextMap = new HashMap<>(); for (AdPutCreativeComponentCostData costData : costDataList) { for (AdPutCreativeComponentCostData.AdPutTencentComponent component : costData.getComponentList()) { String imageUrl = component.getImageUrl(); try { // 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(); text = text.replaceAll("\\n", ""); for (String existText : creativeIdTextMap.keySet()) { if (TitleSimilarCheckUtil.isSimilar(text, existText, TitleSimilarCheckUtil.SIMILARITY_THRESHOLD)) { text = existText; break; } } JSONObject obj = creativeIdTextMap.getOrDefault(text, new JSONObject()); Long cost = obj.getLong("cost"); if (Objects.isNull(cost)) { cost = costData.getCost(); obj.put("cost", cost); obj.put("title", text); obj.put("coverImgUrl", ossImageUrl); } else { cost += costData.getCost(); obj.put("cost", cost); } creativeIdTextMap.put(text, obj); break; } catch (Exception e) { log.error("GzhReplyVideoRefreshJob analysisImageText error, imageUrl:{}", imageUrl, e); } } } return creativeIdTextMap; } private VideoDetail searchVideoByKeyword(List keywords, List searchVideos, List existTitles) { 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 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)) { filter = true; break; } } // 过滤重复标题 if (!filter && CollectionUtils.isNotEmpty(existTitles)) { for (String existTitle : existTitles) { if (TitleSimilarCheckUtil.isSimilar(title, existTitle, 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 searchVideos) { log.info("GzhReplyVideoRefreshJob accountName:{} updateVideoReply, oldVideoIds: {}, replaceVideos: {}", ghDetail.getGhName(), ghDetail.getVideoIds(), JSONObject.toJSONString(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 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(dealTitleLength(videoDetail.getTitle())); videoDetailVo.setCover(videoDetail.getCover()); videoDetailList.add(videoDetailVo); } ghDetailVo.setVideoList(videoDetailList); ghDetailService.updateDetail(ghDetailVo); } private String dealTitleLength(String title) { if (StringUtils.isBlank(title)) { return title; } if (title.length() > 20) { title = title.substring(0, 19) + "…"; } return title; } }