VivoAdvertiserSyncService.java 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. package com.adx.tencent.vivo.service;
  2. import com.adx.tencent.vivo.model.VivoAdvertiserAccountRecord;
  3. import com.adx.tencent.vivo.model.VivoTokenRecord;
  4. import com.adx.tencent.vivo.store.VivoColdStore;
  5. import com.fasterxml.jackson.databind.JsonNode;
  6. import com.fasterxml.jackson.databind.ObjectMapper;
  7. import org.slf4j.Logger;
  8. import org.slf4j.LoggerFactory;
  9. import java.net.URI;
  10. import java.net.URLEncoder;
  11. import java.net.http.HttpClient;
  12. import java.net.http.HttpRequest;
  13. import java.net.http.HttpResponse;
  14. import java.nio.charset.StandardCharsets;
  15. import java.time.Duration;
  16. import java.time.Instant;
  17. import java.util.ArrayList;
  18. import java.util.LinkedHashMap;
  19. import java.util.LinkedHashSet;
  20. import java.util.List;
  21. import java.util.Map;
  22. import java.util.Set;
  23. import java.util.UUID;
  24. import java.util.stream.Collectors;
  25. public class VivoAdvertiserSyncService {
  26. private static final Logger log = LoggerFactory.getLogger(VivoAdvertiserSyncService.class);
  27. private static final String ADVERTISER_QUERY_PATH = "/openapi/v1/account/advertiser/query";
  28. private static final int PAGE_SIZE = 100;
  29. private final String baseUrl;
  30. private final VivoAuthService authService;
  31. private final VivoColdStore coldStore;
  32. private final ObjectMapper objectMapper;
  33. private final HttpClient httpClient;
  34. public VivoAdvertiserSyncService(String baseUrl,
  35. VivoAuthService authService,
  36. VivoColdStore coldStore,
  37. ObjectMapper objectMapper) {
  38. this.baseUrl = trimTrailingSlash(baseUrl);
  39. this.authService = authService;
  40. this.coldStore = coldStore;
  41. this.objectMapper = objectMapper;
  42. this.httpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build();
  43. }
  44. public void syncAllAuthorizedAccounts() {
  45. List<VivoTokenRecord> tokens = authService.listAuthorizedSecondaryAccounts();
  46. if (tokens == null || tokens.isEmpty()) {
  47. return;
  48. }
  49. for (VivoTokenRecord token : tokens) {
  50. if (token == null || token.getAccountId() == null || token.getAccountId().isBlank()) {
  51. continue;
  52. }
  53. syncSecondaryAccount(token.getAccountId().trim());
  54. }
  55. }
  56. public void syncSecondaryAccount(String secondaryAccountId) {
  57. if (secondaryAccountId == null || secondaryAccountId.isBlank()) {
  58. return;
  59. }
  60. String accessToken = authService.getAccessTokenBySecondaryAccountId(secondaryAccountId);
  61. List<VivoAdvertiserAccountRecord> fetched = queryAllAdvertisers(secondaryAccountId, accessToken);
  62. List<String> staleAdvertiserIds = new ArrayList<>(coldStore.listAdvertiserIdsBySecondaryAccountId(secondaryAccountId));
  63. Set<String> freshIds = fetched.stream()
  64. .map(VivoAdvertiserAccountRecord::getAdvertiserId)
  65. .filter(id -> id != null && !id.isBlank())
  66. .collect(Collectors.toCollection(LinkedHashSet::new));
  67. staleAdvertiserIds.removeIf(freshIds::contains);
  68. coldStore.replaceAdvertiserAccounts(secondaryAccountId, fetched);
  69. authService.cacheAdvertiserMappings(fetched);
  70. authService.evictAdvertiserMappings(staleAdvertiserIds);
  71. log.info("[Vivo][广告主同步] secondaryAccountId={} count={} staleCount={}",
  72. secondaryAccountId, fetched.size(), staleAdvertiserIds.size());
  73. }
  74. private List<VivoAdvertiserAccountRecord> queryAllAdvertisers(String secondaryAccountId, String accessToken) {
  75. List<VivoAdvertiserAccountRecord> result = new ArrayList<>();
  76. int pageIndex = 1;
  77. int pageCount = 1;
  78. while (pageIndex <= pageCount) {
  79. PageResult page = queryAdvertiserPage(accessToken, pageIndex);
  80. pageCount = Math.max(page.pageCount, 1);
  81. if (page.records != null) {
  82. result.addAll(page.records);
  83. }
  84. if (page.records == null || page.records.isEmpty()) {
  85. break;
  86. }
  87. pageIndex++;
  88. }
  89. Instant now = Instant.now();
  90. for (VivoAdvertiserAccountRecord record : result) {
  91. record.setSecondaryAccountId(secondaryAccountId);
  92. if (record.getLastSyncAt() == null) {
  93. record.setLastSyncAt(now);
  94. }
  95. if (record.getUpdatedAt() == null) {
  96. record.setUpdatedAt(now);
  97. }
  98. }
  99. return result;
  100. }
  101. private PageResult queryAdvertiserPage(String accessToken, int pageIndex) {
  102. try {
  103. long timestamp = System.currentTimeMillis();
  104. String nonce = UUID.randomUUID().toString().replace("-", "");
  105. String url = baseUrl + ADVERTISER_QUERY_PATH + "?" + toQuery(Map.of(
  106. "access_token", accessToken,
  107. "timestamp", String.valueOf(timestamp),
  108. "nonce", nonce
  109. ));
  110. String body = objectMapper.writeValueAsString(Map.of("pageIndex", pageIndex, "pageSize", PAGE_SIZE));
  111. HttpRequest request = HttpRequest.newBuilder()
  112. .uri(URI.create(url))
  113. .header("Content-Type", "application/json")
  114. .POST(HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8))
  115. .timeout(Duration.ofSeconds(15))
  116. .build();
  117. HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
  118. if (response.statusCode() < 200 || response.statusCode() >= 300) {
  119. throw new IllegalStateException("HTTP status: " + response.statusCode() + ", body=" + response.body());
  120. }
  121. JsonNode root = objectMapper.readTree(response.body());
  122. int code = root.path("code").asInt(Integer.MIN_VALUE);
  123. if (code != 0) {
  124. throw new IllegalStateException("code=" + code + ", message=" + root.path("message").asText() + ", body=" + response.body());
  125. }
  126. JsonNode data = root.path("data");
  127. JsonNode pageInfo = data.path("pageInfo");
  128. int pageCount = pageInfo.path("pageCount").asInt(1);
  129. List<VivoAdvertiserAccountRecord> records = new ArrayList<>();
  130. JsonNode list = data.path("list");
  131. if (list.isArray()) {
  132. Instant now = Instant.now();
  133. for (JsonNode item : list) {
  134. String advertiserId = text(item, "uuid");
  135. if (advertiserId == null || advertiserId.isBlank()) {
  136. continue;
  137. }
  138. VivoAdvertiserAccountRecord record = new VivoAdvertiserAccountRecord();
  139. record.setAdvertiserId(advertiserId);
  140. record.setAdvertiserName(text(item, "name"));
  141. record.setAdvertiserStatus(asInt(item.get("status")));
  142. record.setLastSyncAt(now);
  143. record.setUpdatedAt(now);
  144. records.add(record);
  145. }
  146. }
  147. PageResult pageResult = new PageResult();
  148. pageResult.pageCount = pageCount;
  149. pageResult.records = records;
  150. return pageResult;
  151. } catch (Exception e) {
  152. throw new RuntimeException("sync vivo advertiser page failed: " + e.getMessage(), e);
  153. }
  154. }
  155. private static String toQuery(Map<String, String> params) {
  156. StringBuilder sb = new StringBuilder();
  157. boolean first = true;
  158. for (Map.Entry<String, String> entry : params.entrySet()) {
  159. if (entry.getValue() == null || entry.getValue().isBlank()) continue;
  160. if (!first) sb.append('&');
  161. first = false;
  162. sb.append(URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8))
  163. .append('=')
  164. .append(URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8));
  165. }
  166. return sb.toString();
  167. }
  168. private static String text(JsonNode node, String field) {
  169. JsonNode child = node == null ? null : node.get(field);
  170. if (child == null || child.isNull()) {
  171. return null;
  172. }
  173. String value = child.asText();
  174. return value == null || value.isBlank() ? null : value.trim();
  175. }
  176. private static Integer asInt(JsonNode node) {
  177. if (node == null || node.isNull()) {
  178. return null;
  179. }
  180. if (node.isInt() || node.isLong()) {
  181. return node.intValue();
  182. }
  183. try {
  184. return Integer.parseInt(node.asText().trim());
  185. } catch (Exception e) {
  186. return null;
  187. }
  188. }
  189. private static String trimTrailingSlash(String value) {
  190. if (value == null || value.isBlank()) {
  191. return "https://marketing-api.vivo.com.cn";
  192. }
  193. String result = value.trim();
  194. while (result.endsWith("/")) {
  195. result = result.substring(0, result.length() - 1);
  196. }
  197. return result;
  198. }
  199. private static class PageResult {
  200. private int pageCount;
  201. private List<VivoAdvertiserAccountRecord> records;
  202. }
  203. }