|
@@ -11,10 +11,14 @@ import cn.com.ctop.job.adq.data.utils.HttpUtils;
|
|
|
import cn.com.ctop.job.adq.utils.RandomStringGenerator;
|
|
import cn.com.ctop.job.adq.utils.RandomStringGenerator;
|
|
|
import com.alibaba.fastjson.JSONArray;
|
|
import com.alibaba.fastjson.JSONArray;
|
|
|
import com.alibaba.fastjson.JSONObject;
|
|
import com.alibaba.fastjson.JSONObject;
|
|
|
|
|
+import com.google.common.collect.Lists;
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
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 org.springframework.stereotype.Service;
|
|
|
|
|
|
|
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
import java.time.Instant;
|
|
import java.time.Instant;
|
|
|
import java.util.ArrayList;
|
|
import java.util.ArrayList;
|
|
|
import java.util.HashMap;
|
|
import java.util.HashMap;
|
|
@@ -24,10 +28,15 @@ import java.util.Map;
|
|
|
@Slf4j
|
|
@Slf4j
|
|
|
@Service
|
|
@Service
|
|
|
public class AdqMaterialReportServiceImpl implements IAdqMaterialReportService {
|
|
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
|
|
@Autowired
|
|
|
private AdqMaterialReportMapper adqMaterialReportMapper;
|
|
private AdqMaterialReportMapper adqMaterialReportMapper;
|
|
|
@Autowired
|
|
@Autowired
|
|
|
private AdqErrorLogMapper adqErrorLogMapper;
|
|
private AdqErrorLogMapper adqErrorLogMapper;
|
|
|
|
|
+ @Autowired
|
|
|
|
|
+ private StringRedisTemplate stringRedisTemplate;
|
|
|
|
|
|
|
|
@Override
|
|
@Override
|
|
|
public void getAdqMaterialDailyReport(Long advertiserId, String accessToken, String startDate, String endDate, Integer page, Integer isLog, Long errId, Integer isSend) {
|
|
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)) {
|
|
if (!Check.isNull(reports)) {
|
|
|
|
|
+ fillMediaLinks(advertiserId, accessToken, reports);
|
|
|
adqMaterialReportMapper.insertAdqMaterialReportDaily(reports);
|
|
adqMaterialReportMapper.insertAdqMaterialReportDaily(reports);
|
|
|
}
|
|
}
|
|
|
if (page < totalPage) {
|
|
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;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|