package com.adx.tencent.vivo.service; import com.adx.tencent.vivo.model.VivoAdvertiserAccountRecord; import com.adx.tencent.vivo.model.VivoTokenRecord; import com.adx.tencent.vivo.store.VivoColdStore; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.time.Instant; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; public class VivoAdvertiserSyncService { private static final Logger log = LoggerFactory.getLogger(VivoAdvertiserSyncService.class); private static final String ADVERTISER_QUERY_PATH = "/openapi/v1/account/advertiser/query"; private static final int PAGE_SIZE = 100; private final String baseUrl; private final VivoAuthService authService; private final VivoColdStore coldStore; private final ObjectMapper objectMapper; private final HttpClient httpClient; public VivoAdvertiserSyncService(String baseUrl, VivoAuthService authService, VivoColdStore coldStore, ObjectMapper objectMapper) { this.baseUrl = trimTrailingSlash(baseUrl); this.authService = authService; this.coldStore = coldStore; this.objectMapper = objectMapper; this.httpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build(); } public void syncAllAuthorizedAccounts() { List tokens = authService.listAuthorizedSecondaryAccounts(); if (tokens == null || tokens.isEmpty()) { return; } for (VivoTokenRecord token : tokens) { if (token == null || token.getAccountId() == null || token.getAccountId().isBlank()) { continue; } syncSecondaryAccount(token.getAccountId().trim()); } } public void syncSecondaryAccount(String secondaryAccountId) { if (secondaryAccountId == null || secondaryAccountId.isBlank()) { return; } String accessToken = authService.getAccessTokenBySecondaryAccountId(secondaryAccountId); List fetched = queryAllAdvertisers(secondaryAccountId, accessToken); List staleAdvertiserIds = new ArrayList<>(coldStore.listAdvertiserIdsBySecondaryAccountId(secondaryAccountId)); Set freshIds = fetched.stream() .map(VivoAdvertiserAccountRecord::getAdvertiserId) .filter(id -> id != null && !id.isBlank()) .collect(Collectors.toCollection(LinkedHashSet::new)); staleAdvertiserIds.removeIf(freshIds::contains); coldStore.replaceAdvertiserAccounts(secondaryAccountId, fetched); authService.cacheAdvertiserMappings(fetched); authService.evictAdvertiserMappings(staleAdvertiserIds); log.info("[Vivo][广告主同步] secondaryAccountId={} count={} staleCount={}", secondaryAccountId, fetched.size(), staleAdvertiserIds.size()); } private List queryAllAdvertisers(String secondaryAccountId, String accessToken) { List result = new ArrayList<>(); int pageIndex = 1; int pageCount = 1; while (pageIndex <= pageCount) { PageResult page = queryAdvertiserPage(accessToken, pageIndex); pageCount = Math.max(page.pageCount, 1); if (page.records != null) { result.addAll(page.records); } if (page.records == null || page.records.isEmpty()) { break; } pageIndex++; } Instant now = Instant.now(); for (VivoAdvertiserAccountRecord record : result) { record.setSecondaryAccountId(secondaryAccountId); if (record.getLastSyncAt() == null) { record.setLastSyncAt(now); } if (record.getUpdatedAt() == null) { record.setUpdatedAt(now); } } return result; } private PageResult queryAdvertiserPage(String accessToken, int pageIndex) { try { long timestamp = System.currentTimeMillis(); String nonce = UUID.randomUUID().toString().replace("-", ""); String url = baseUrl + ADVERTISER_QUERY_PATH + "?" + toQuery(Map.of( "access_token", accessToken, "timestamp", String.valueOf(timestamp), "nonce", nonce )); String body = objectMapper.writeValueAsString(Map.of("pageIndex", pageIndex, "pageSize", PAGE_SIZE)); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(url)) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) .timeout(Duration.ofSeconds(15)) .build(); HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); if (response.statusCode() < 200 || response.statusCode() >= 300) { throw new IllegalStateException("HTTP status: " + response.statusCode() + ", body=" + response.body()); } JsonNode root = objectMapper.readTree(response.body()); int code = root.path("code").asInt(Integer.MIN_VALUE); if (code != 0) { throw new IllegalStateException("code=" + code + ", message=" + root.path("message").asText() + ", body=" + response.body()); } JsonNode data = root.path("data"); JsonNode pageInfo = data.path("pageInfo"); int pageCount = pageInfo.path("pageCount").asInt(1); List records = new ArrayList<>(); JsonNode list = data.path("list"); if (list.isArray()) { Instant now = Instant.now(); for (JsonNode item : list) { String advertiserId = text(item, "uuid"); if (advertiserId == null || advertiserId.isBlank()) { continue; } VivoAdvertiserAccountRecord record = new VivoAdvertiserAccountRecord(); record.setAdvertiserId(advertiserId); record.setAdvertiserName(text(item, "name")); record.setAdvertiserStatus(asInt(item.get("status"))); record.setLastSyncAt(now); record.setUpdatedAt(now); records.add(record); } } PageResult pageResult = new PageResult(); pageResult.pageCount = pageCount; pageResult.records = records; return pageResult; } catch (Exception e) { throw new RuntimeException("sync vivo advertiser page failed: " + e.getMessage(), e); } } private static String toQuery(Map params) { StringBuilder sb = new StringBuilder(); boolean first = true; for (Map.Entry entry : params.entrySet()) { if (entry.getValue() == null || entry.getValue().isBlank()) continue; if (!first) sb.append('&'); first = false; sb.append(URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8)) .append('=') .append(URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8)); } return sb.toString(); } private static String text(JsonNode node, String field) { JsonNode child = node == null ? null : node.get(field); if (child == null || child.isNull()) { return null; } String value = child.asText(); return value == null || value.isBlank() ? null : value.trim(); } private static Integer asInt(JsonNode node) { if (node == null || node.isNull()) { return null; } if (node.isInt() || node.isLong()) { return node.intValue(); } try { return Integer.parseInt(node.asText().trim()); } catch (Exception e) { return null; } } private static String trimTrailingSlash(String value) { if (value == null || value.isBlank()) { return "https://marketing-api.vivo.com.cn"; } String result = value.trim(); while (result.endsWith("/")) { result = result.substring(0, result.length() - 1); } return result; } private static class PageResult { private int pageCount; private List records; } }