yumeng 1 månad sedan
förälder
incheckning
b5d3462572

+ 27 - 0
src/main/java/com/adx/tencent/config/AppProperties.java

@@ -76,6 +76,9 @@ public class AppProperties {
     private String vivoTagEventSyncRedisPrefix = "";
     private String vivoRedirectUri;
     private String vivoTokenRedisPrefix = "adx:vivo:token:";
+    private boolean vivoTokenRefreshEnabled = true;
+    private Duration vivoTokenRefreshInterval = Duration.ofHours(1);
+    private Duration vivoTokenRefreshAhead = Duration.ofDays(1);
     private boolean vivoAdvertiserSyncEnabled = false;
     private Duration vivoAdvertiserSyncInterval = Duration.ofMinutes(10);
     private String vivoAdvertiserRedisPrefix = "adx:vivo:advertiser:";
@@ -562,6 +565,30 @@ public class AppProperties {
         this.vivoTokenRedisPrefix = v;
     }
 
+    public boolean isVivoTokenRefreshEnabled() {
+        return vivoTokenRefreshEnabled;
+    }
+
+    public void setVivoTokenRefreshEnabled(boolean v) {
+        this.vivoTokenRefreshEnabled = v;
+    }
+
+    public Duration getVivoTokenRefreshInterval() {
+        return vivoTokenRefreshInterval;
+    }
+
+    public void setVivoTokenRefreshInterval(Duration v) {
+        this.vivoTokenRefreshInterval = v;
+    }
+
+    public Duration getVivoTokenRefreshAhead() {
+        return vivoTokenRefreshAhead;
+    }
+
+    public void setVivoTokenRefreshAhead(Duration v) {
+        this.vivoTokenRefreshAhead = v;
+    }
+
     public boolean isVivoAdvertiserSyncEnabled() {
         return vivoAdvertiserSyncEnabled;
     }

+ 12 - 0
src/main/java/com/adx/tencent/vivo/controller/VivoTrackingController.java

@@ -5,6 +5,7 @@ import com.adx.tencent.baidu.model.NormalizedBidResponse;
 import com.adx.tencent.baidu.model.TrackingResult;
 import com.adx.tencent.vivo.model.VivoBidRecord;
 import com.adx.tencent.vivo.model.VivoTrackingRecord;
+import com.adx.tencent.vivo.service.VivoAdvertiserSyncService;
 import com.adx.tencent.vivo.service.VivoAuthService;
 import com.adx.tencent.vivo.service.VivoPlacement;
 import com.adx.tencent.vivo.store.VivoHotStore;
@@ -44,17 +45,20 @@ public class VivoTrackingController {
     private final VivoHotStore hotStore;
     private final VivoPlacement placement;
     private final VivoAuthService authService;
+    private final VivoAdvertiserSyncService advertiserSyncService;
     private final ObjectMapper objectMapper;
 
     public VivoTrackingController(AdxClient adxClient,
                                   @Nullable VivoHotStore hotStore,
                                   @Nullable VivoPlacement placement,
                                   @Nullable VivoAuthService authService,
+                                  @Nullable VivoAdvertiserSyncService advertiserSyncService,
                                   ObjectMapper objectMapper) {
         this.adxClient = adxClient;
         this.hotStore = hotStore;
         this.placement = placement;
         this.authService = authService;
+        this.advertiserSyncService = advertiserSyncService;
         this.objectMapper = objectMapper;
     }
 
@@ -104,6 +108,14 @@ public class VivoTrackingController {
             throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "missing vivo code");
         }
         var token = authService.authorizeSecondaryAccount(secondaryAccountId, clientId, code);
+        if (advertiserSyncService != null) {
+            try {
+                advertiserSyncService.syncSecondaryAccount(token.getAccountId());
+            } catch (Exception e) {
+                log.error("[Vivo][授权] secondaryAccountId={} immediate advertiser sync failed: {}",
+                        token.getAccountId(), e.getMessage(), e);
+            }
+        }
         return Map.of(
                 "secondaryAccountId", token.getAccountId(),
                 "clientId", token.getClientId() == null ? "" : token.getClientId(),

+ 52 - 0
src/main/java/com/adx/tencent/vivo/service/VivoAuthService.java

@@ -145,6 +145,25 @@ public class VivoAuthService {
         return coldStore.listTokens();
     }
 
+    public void refreshTokensExpiringWithin(Duration threshold) {
+        long thresholdMs = normalizeThresholdMs(threshold);
+        List<VivoTokenRecord> tokens = listAuthorizedSecondaryAccounts();
+        if (tokens == null || tokens.isEmpty()) {
+            return;
+        }
+        for (VivoTokenRecord token : tokens) {
+            if (token == null || token.getAccountId() == null || token.getAccountId().isBlank()) {
+                continue;
+            }
+            try {
+                refreshTokenIfExpiringWithin(token.getAccountId().trim(), thresholdMs);
+            } catch (Exception e) {
+                log.error("[Vivo][Token] secondaryAccountId={} proactive refresh failed: {}",
+                        token.getAccountId(), e.getMessage(), e);
+            }
+        }
+    }
+
     public void cacheAdvertiserMappings(List<VivoAdvertiserAccountRecord> records) {
         if (records == null || records.isEmpty()) {
             return;
@@ -252,6 +271,28 @@ public class VivoAuthService {
         return record;
     }
 
+    private void refreshTokenIfExpiringWithin(String secondaryAccountId, long thresholdMs) {
+        synchronized (locks.computeIfAbsent(secondaryAccountId, ignored -> new Object())) {
+            VivoTokenRecord record = loadToken(secondaryAccountId);
+            if (record == null) {
+                return;
+            }
+            long now = System.currentTimeMillis();
+            if (!expiresWithin(record.getTokenExpireAt(), now, thresholdMs)) {
+                return;
+            }
+            if (isUsable(record.getRefreshToken(), record.getRefreshTokenExpireAt(), now)) {
+                long remainingMs = record.getTokenExpireAt() == null ? -1L : record.getTokenExpireAt() - now;
+                log.info("[Vivo][Token] secondaryAccountId={} token expires soon, proactive refresh | remainingMs={} | thresholdMs={}",
+                        secondaryAccountId, remainingMs, thresholdMs);
+                refresh(secondaryAccountId, record.getRefreshToken());
+                return;
+            }
+            log.warn("[Vivo][Token] secondaryAccountId={} token expires within threshold but refresh_token is unavailable | tokenExpireAt={} | refreshTokenExpireAt={}",
+                    secondaryAccountId, record.getTokenExpireAt(), record.getRefreshTokenExpireAt());
+        }
+    }
+
     private VivoTokenRecord buildTokenRecord(AgentAccountProfile profile,
                                              TokenPayload payload,
                                              String secondaryAccountId,
@@ -477,6 +518,17 @@ public class VivoAuthService {
         return token != null && !token.isBlank() && expireAt != null && expireAt - now > REFRESH_AHEAD_MS;
     }
 
+    private static boolean expiresWithin(Long expireAt, long now, long thresholdMs) {
+        return expireAt != null && expireAt - now <= thresholdMs;
+    }
+
+    private static long normalizeThresholdMs(Duration threshold) {
+        if (threshold == null || threshold.isNegative() || threshold.isZero()) {
+            return Duration.ofDays(1).toMillis();
+        }
+        return threshold.toMillis();
+    }
+
     private static String toQuery(Map<String, String> params) {
         StringBuilder sb = new StringBuilder();
         boolean first = true;

+ 27 - 0
src/main/java/com/adx/tencent/vivo/service/VivoBackgroundTasks.java

@@ -21,6 +21,7 @@ public class VivoBackgroundTasks implements ApplicationRunner {
 
     @Autowired private AppProperties props;
     @Autowired(required = false) private VivoColdWorker vivoColdWorker;
+    @Autowired(required = false) private VivoAuthService vivoAuthService;
     @Autowired(required = false) private VivoRetryService vivoRetryService;
     @Autowired(required = false) private VivoTagEventSyncService vivoTagEventSyncService;
     @Autowired(required = false) private VivoAdvertiserSyncService vivoAdvertiserSyncService;
@@ -62,6 +63,20 @@ public class VivoBackgroundTasks implements ApplicationRunner {
             }
         }
 
+        if (vivoAuthService != null && props.isVivoTokenRefreshEnabled()) {
+            if (props.isSkipLeaderElection() || leaderElection == null) {
+                executor.submit(() -> runTokenRefresh(() -> stopped.get()));
+            } else {
+                executor.submit(() -> leaderElection.run(
+                        "adx:lock:vivo:token-refresh",
+                        props.getTaskLockTtl(), props.getTaskLockRenewInterval(), props.getTaskLockRetryInterval(),
+                        stopped::get,
+                        this::runTokenRefresh,
+                        e -> log.error("vivo token refresh leader: {}", e.getMessage(), e)
+                ));
+            }
+        }
+
         if (vivoTagEventSyncService != null && props.isVivoTagEventSyncEnabled()) {
             if (props.isSkipLeaderElection() || leaderElection == null) {
                 executor.submit(() -> runTagEventSync(() -> stopped.get()));
@@ -91,6 +106,18 @@ public class VivoBackgroundTasks implements ApplicationRunner {
         }
     }
 
+    private void runTokenRefresh(LeaderElection.StopSignal stopSignal) {
+        long intervalMs = props.getVivoTokenRefreshInterval().toMillis();
+        while (!stopSignal.isStopped()) {
+            try {
+                vivoAuthService.refreshTokensExpiringWithin(props.getVivoTokenRefreshAhead());
+            } catch (Exception e) {
+                log.error("vivo token refresh: {}", e.getMessage(), e);
+            }
+            sleepResponsive(intervalMs, stopSignal);
+        }
+    }
+
     private void runRetry(LeaderElection.StopSignal stopSignal) {
         long intervalMs = props.getVivoCallbackRetryInterval().toMillis();
         while (!stopSignal.isStopped()) {

+ 3 - 0
src/main/resources/application-dev.yml

@@ -100,6 +100,9 @@ adx:
   vivo-tag-event-sync-redis-prefix: "adx:vivo:tag-event:"
   vivo-redirect-uri: "/vivo/callback"
   vivo-token-redis-prefix: "adx:vivo:token:"
+  vivo-token-refresh-enabled: true
+  vivo-token-refresh-interval: 3600s
+  vivo-token-refresh-ahead: 86400s
   vivo-advertiser-sync-enabled: true
   vivo-advertiser-sync-interval: 600s
   vivo-advertiser-redis-prefix: "adx:vivo:advertiser:"

+ 3 - 0
src/main/resources/application-prod.yml

@@ -105,6 +105,9 @@ adx:
   vivo-tag-event-sync-redis-prefix: "adx:vivo:tag-event:"
   vivo-redirect-uri: "/vivo/callback"
   vivo-token-redis-prefix: "adx:vivo:token:"
+  vivo-token-refresh-enabled: true
+  vivo-token-refresh-interval: 3600s
+  vivo-token-refresh-ahead: 86400s
   vivo-advertiser-sync-enabled: true
   vivo-advertiser-sync-interval: 600s
   vivo-advertiser-redis-prefix: "adx:vivo:advertiser:"

+ 3 - 0
src/main/resources/application-test.yml

@@ -104,6 +104,9 @@ adx:
   vivo-tag-event-sync-redis-prefix: "adx:vivo:tag-event:"
   vivo-redirect-uri: "/vivo/callback"
   vivo-token-redis-prefix: "adx:vivo:token:"
+  vivo-token-refresh-enabled: true
+  vivo-token-refresh-interval: 3600s
+  vivo-token-refresh-ahead: 86400s
   vivo-advertiser-sync-enabled: true
   vivo-advertiser-sync-interval: 600s
   vivo-advertiser-redis-prefix: "adx:vivo:advertiser:"