yumeng 2 giorni fa
parent
commit
9a48ee1c5e

+ 4 - 0
job-adq/src/main/java/cn/com/ctop/job/adq/data/mapper/xml/AdqMaterialReportMapper.xml

@@ -8,6 +8,8 @@
         advertiser_id,
         material_id,
         material_type,
+        material_link,
+        cover_link,
         cost,
         view_count,
         valid_click_count,
@@ -36,6 +38,8 @@
             #{item.advertiser_id},
             #{item.material_id},
             #{item.material_type},
+            #{item.material_link},
+            #{item.cover_link},
             #{item.cost},
             #{item.view_count},
             #{item.valid_click_count},

+ 207 - 0
job-adq/src/main/java/cn/com/ctop/job/adq/data/service/impl/AdqMaterialReportServiceImpl.java

@@ -11,10 +11,14 @@ import cn.com.ctop.job.adq.data.utils.HttpUtils;
 import cn.com.ctop.job.adq.utils.RandomStringGenerator;
 import com.alibaba.fastjson.JSONArray;
 import com.alibaba.fastjson.JSONObject;
+import com.google.common.collect.Lists;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisCallback;
+import org.springframework.data.redis.core.StringRedisTemplate;
 import org.springframework.stereotype.Service;
 
+import java.nio.charset.StandardCharsets;
 import java.time.Instant;
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -24,10 +28,15 @@ import java.util.Map;
 @Slf4j
 @Service
 public class AdqMaterialReportServiceImpl implements IAdqMaterialReportService {
+    private static final String MATERIAL_LINK_KEY_PREFIX = "adq-material-link-key-";
+    private static final long MATERIAL_LINK_EXPIRE_SECONDS = 30L * 24 * 60 * 60;
+
     @Autowired
     private AdqMaterialReportMapper adqMaterialReportMapper;
     @Autowired
     private AdqErrorLogMapper adqErrorLogMapper;
+    @Autowired
+    private StringRedisTemplate stringRedisTemplate;
 
     @Override
     public void getAdqMaterialDailyReport(Long advertiserId, String accessToken, String startDate, String endDate, Integer page, Integer isLog, Long errId, Integer isSend) {
@@ -122,6 +131,7 @@ public class AdqMaterialReportServiceImpl implements IAdqMaterialReportService {
                 }
             }
             if (!Check.isNull(reports)) {
+                fillMediaLinks(advertiserId, accessToken, reports);
                 adqMaterialReportMapper.insertAdqMaterialReportDaily(reports);
             }
             if (page < totalPage) {
@@ -153,4 +163,201 @@ public class AdqMaterialReportServiceImpl implements IAdqMaterialReportService {
             }
         }
     }
+
+    /**
+     * 批量查询素材链接并回填:先批量查Redis缓存,未命中的再批量调腾讯接口并回写缓存(30天)。
+     * 图片取preview_url填material_link;视频取preview_url填material_link、
+     * key_frame_image_url(首帧缩略图)填cover_link。查询失败不阻塞日报落库。
+     */
+    private void fillMediaLinks(Long advertiserId, String accessToken, List<JSONObject> reports) {
+        try {
+            List<String> imageIds = new ArrayList<>();
+            List<String> videoIds = new ArrayList<>();
+            for (JSONObject report : reports) {
+                if ("IMAGE".equals(report.getString("material_type"))) {
+                    imageIds.add(report.getString("material_id"));
+                } else {
+                    videoIds.add(report.getString("material_id"));
+                }
+            }
+            Map<String, JSONObject> imageLinkMap = getMaterialLinksFromRedis(imageIds);
+            Map<String, JSONObject> videoLinkMap = getMaterialLinksFromRedis(videoIds);
+            Map<String, String> imageUrlMap = getImageUrlMap(advertiserId, accessToken, filterMissIds(imageIds, imageLinkMap));
+            Map<String, JSONObject> videoInfoMap = getVideoInfoMap(advertiserId, accessToken, filterMissIds(videoIds, videoLinkMap));
+            Map<String, JSONObject> imageLinkToSave = new HashMap<>();
+            for (Map.Entry<String, String> entry : imageUrlMap.entrySet()) {
+                JSONObject linkInfo = new JSONObject().fluentPut("material_link", entry.getValue());
+                imageLinkMap.put(entry.getKey(), linkInfo);
+                imageLinkToSave.put(entry.getKey(), linkInfo);
+            }
+            Map<String, JSONObject> videoLinkToSave = new HashMap<>();
+            for (Map.Entry<String, JSONObject> entry : videoInfoMap.entrySet()) {
+                JSONObject linkInfo = new JSONObject()
+                        .fluentPut("material_link", entry.getValue().getString("preview_url"))
+                        .fluentPut("cover_link", entry.getValue().getString("key_frame_image_url"));
+                videoLinkMap.put(entry.getKey(), linkInfo);
+                videoLinkToSave.put(entry.getKey(), linkInfo);
+            }
+            saveMaterialLinksToRedis(imageLinkToSave);
+            saveMaterialLinksToRedis(videoLinkToSave);
+            for (JSONObject report : reports) {
+                JSONObject linkInfo = "IMAGE".equals(report.getString("material_type"))
+                        ? imageLinkMap.get(report.getString("material_id"))
+                        : videoLinkMap.get(report.getString("material_id"));
+                if (!Check.isNull(linkInfo)) {
+                    report.put("material_link", linkInfo.getString("material_link"));
+                    report.put("cover_link", linkInfo.getString("cover_link"));
+                }
+            }
+        } catch (Exception e) {
+            log.error("回填素材链接失败,account_id:{}", advertiserId, e);
+        }
+    }
+
+    /**
+     * 批量查素材链接缓存:key=adq-material-link-key-{materialId}(素材ID平台级唯一,跨账户共享缓存减少腾讯接口调用),
+     * value={"material_link":"...","cover_link":"..."}
+     */
+    private Map<String, JSONObject> getMaterialLinksFromRedis(List<String> materialIds) {
+        Map<String, JSONObject> cacheMap = new HashMap<>();
+        if (Check.isNull(materialIds)) {
+            return cacheMap;
+        }
+        try {
+            List<String> keys = new ArrayList<>();
+            for (String id : materialIds) {
+                keys.add(MATERIAL_LINK_KEY_PREFIX + id);
+            }
+            List<String> values = stringRedisTemplate.opsForValue().multiGet(keys);
+            if (!Check.isNull(values)) {
+                for (int i = 0; i < keys.size() && i < values.size(); i++) {
+                    String value = values.get(i);
+                    if (value != null) {
+                        cacheMap.put(materialIds.get(i), JSONObject.parseObject(value));
+                    }
+                }
+            }
+        } catch (Exception e) {
+            log.error("查询素材链接缓存异常", e);
+        }
+        return cacheMap;
+    }
+
+    private void saveMaterialLinksToRedis(Map<String, JSONObject> linkMap) {
+        if (Check.isNullMap(linkMap)) {
+            return;
+        }
+        try {
+            stringRedisTemplate.executePipelined((RedisCallback<Object>) connection -> {
+                for (Map.Entry<String, JSONObject> entry : linkMap.entrySet()) {
+                    byte[] key = (MATERIAL_LINK_KEY_PREFIX + entry.getKey()).getBytes(StandardCharsets.UTF_8);
+                    connection.set(key, entry.getValue().toJSONString().getBytes(StandardCharsets.UTF_8));
+                    connection.expire(key, MATERIAL_LINK_EXPIRE_SECONDS);
+                }
+                return null;
+            });
+        } catch (Exception e) {
+            log.error("保存素材链接缓存异常", e);
+        }
+    }
+
+    private List<String> filterMissIds(List<String> materialIds, Map<String, JSONObject> cacheMap) {
+        List<String> missIds = new ArrayList<>();
+        for (String id : materialIds) {
+            if (!cacheMap.containsKey(id)) {
+                missIds.add(id);
+            }
+        }
+        return missIds;
+    }
+
+    private Map<String, String> getImageUrlMap(Long advertiserId, String accessToken, List<String> imageIds) {
+        Map<String, String> urlMap = new HashMap<>();
+        if (Check.isNull(imageIds)) {
+            return urlMap;
+        }
+        try {
+            for (List<String> chunk : Lists.partition(imageIds, 100)) {
+                Map<String, Object> paramsMap = buildMediaParams(accessToken, advertiserId);
+                paramsMap.put("filtering", buildIdFiltering("image_id", chunk).toJSONString());
+                paramsMap.put("page", 1);
+                paramsMap.put("page_size", 100);
+                String s = HttpUtils.httpGet("https://api.e.qq.com/v3.0/images/get", paramsMap, null);
+                JSONObject jsonObject = JSONObject.parseObject(s);
+                if (Check.isNull(jsonObject) || jsonObject.getInteger("code") != 0) {
+                    log.error("查询图片链接失败,account_id:{},返回信息:{}", advertiserId, jsonObject);
+                    continue;
+                }
+                JSONObject data = jsonObject.getJSONObject("data");
+                if (Check.isNull(data)) {
+                    continue;
+                }
+                JSONArray list = data.getJSONArray("list");
+                if (!Check.isNull(list)) {
+                    for (int i = 0; i < list.size(); i++) {
+                        JSONObject item = list.getJSONObject(i);
+                        urlMap.put(item.getString("image_id"), item.getString("preview_url"));
+                    }
+                }
+            }
+        } catch (Exception e) {
+            log.error("查询图片链接异常,account_id:{}", advertiserId, e);
+        }
+        return urlMap;
+    }
+
+    private Map<String, JSONObject> getVideoInfoMap(Long advertiserId, String accessToken, List<String> videoIds) {
+        Map<String, JSONObject> videoMap = new HashMap<>();
+        if (Check.isNull(videoIds)) {
+            return videoMap;
+        }
+        try {
+            for (List<String> chunk : Lists.partition(videoIds, 100)) {
+                Map<String, Object> paramsMap = buildMediaParams(accessToken, advertiserId);
+                // videos/get批量过滤字段为media_id,其取值即视频ID
+                paramsMap.put("filtering", buildIdFiltering("media_id", chunk).toJSONString());
+                paramsMap.put("page", 1);
+                paramsMap.put("page_size", 100);
+                String s = HttpUtils.httpGet("https://api.e.qq.com/v3.0/videos/get", paramsMap, null);
+                JSONObject jsonObject = JSONObject.parseObject(s);
+                if (Check.isNull(jsonObject) || jsonObject.getInteger("code") != 0) {
+                    log.error("查询视频链接失败,account_id:{},返回信息:{}", advertiserId, jsonObject);
+                    continue;
+                }
+                JSONObject data = jsonObject.getJSONObject("data");
+                if (Check.isNull(data)) {
+                    continue;
+                }
+                JSONArray list = data.getJSONArray("list");
+                if (!Check.isNull(list)) {
+                    for (int i = 0; i < list.size(); i++) {
+                        JSONObject item = list.getJSONObject(i);
+                        videoMap.put(String.valueOf(item.getLong("video_id")), item);
+                    }
+                }
+            }
+        } catch (Exception e) {
+            log.error("查询视频链接异常,account_id:{}", advertiserId, e);
+        }
+        return videoMap;
+    }
+
+    private Map<String, Object> buildMediaParams(String accessToken, Long advertiserId) {
+        Map<String, Object> paramsMap = new HashMap<>();
+        paramsMap.put("access_token", accessToken);
+        paramsMap.put("timestamp", String.valueOf(Instant.now().getEpochSecond()));
+        paramsMap.put("nonce", RandomStringGenerator.generateRandomString(32));
+        paramsMap.put("account_id", advertiserId);
+        return paramsMap;
+    }
+
+    private JSONArray buildIdFiltering(String field, List<String> ids) {
+        JSONObject condition = new JSONObject();
+        condition.put("field", field);
+        condition.put("operator", "IN");
+        condition.put("values", ids);
+        JSONArray filtering = new JSONArray();
+        filtering.add(condition);
+        return filtering;
+    }
 }