yumeng 2 semanas atrás
pai
commit
d48f98e99d

+ 63 - 0
src/main/java/com/adx/tencent/AppConfiguration.java

@@ -28,6 +28,7 @@ import com.adx.tencent.kuaishou.store.KuaishouColdStore;
 import com.adx.tencent.kuaishou.store.KuaishouHotStore;
 import com.adx.tencent.leader.LeaderElection;
 import com.adx.tencent.report.AdBidReportStore;
+import com.adx.tencent.rta.RtaOrderSyncService;
 import com.adx.tencent.rta.RtaOrderStore;
 import com.adx.tencent.tagsync.AccountTagEventResolver;
 import com.adx.tencent.tagsync.AccountTagEventSyncService;
@@ -659,6 +660,21 @@ public class AppConfiguration {
     }
 
     @Bean
+    public RtaOrderSyncService rtaOrderSyncService(@Nullable RtaOrderStore rtaOrderStore,
+                                                   StringRedisTemplate redisTemplate) {
+        if (rtaOrderStore == null) return null;
+        Duration ttl = props.getRtaOrderCacheTtl();
+        if (ttl == null || ttl.isZero() || ttl.isNegative()) {
+            Duration interval = props.getRtaOrderSyncInterval() == null
+                    ? Duration.ofMinutes(5)
+                    : props.getRtaOrderSyncInterval();
+            ttl = interval.multipliedBy(3);
+        }
+        return new RtaOrderSyncService(rtaOrderStore, redisTemplate,
+                props.getRtaOrderRedisPrefix(), ttl);
+    }
+
+    @Bean
     public AdBidReportStore adBidReportStore(@Nullable DataSource tidbDataSource) {
         if (tidbDataSource == null) return null;
         String mainSchema = resolveDatabaseName(tidbDataSource, props.getTidbUrl());
@@ -772,6 +788,7 @@ public class AppConfiguration {
         @Autowired(required = false) private RetryService retryService;
         @Autowired(required = false) private TagEventSyncService tagEventSyncService;
         @Autowired(required = false) private AccountTagEventSyncService accountTagEventSyncService;
+        @Autowired(required = false) private RtaOrderSyncService rtaOrderSyncService;
         @Autowired(required = false) private LeaderElection leaderElection;
 
         private final AtomicBoolean stopped = new AtomicBoolean(false);
@@ -885,6 +902,28 @@ public class AppConfiguration {
                 taskLog.warn("Account tag event sync NOT started: service={}, enabled={}",
                         accountTagEventSyncService != null, props.isAccountTagEventSyncEnabled());
             }
+
+            // RTA order_id 配置同步(荣耀/vivo 共用)
+            if (rtaOrderSyncService != null && props.isRtaOrderSyncEnabled()) {
+                if (props.isSkipLeaderElection()) {
+                    executor.submit(() -> runRtaOrderSync(() -> stopped.get()));
+                    taskLog.info("RTA order sync started (skip leader election)");
+                } else if (leaderElection != null) {
+                    executor.submit(() ->
+                        leaderElection.run(
+                            "adx:lock:rta:order-sync",
+                            props.getTaskLockTtl(), props.getTaskLockRenewInterval(), props.getTaskLockRetryInterval(),
+                            stopped::get,
+                            (jobStop) -> runRtaOrderSync(jobStop),
+                            e -> taskLog.error("rta order sync leader: {}", e.getMessage(), e)
+                        )
+                    );
+                    taskLog.info("RTA order sync leader election started");
+                }
+            } else {
+                taskLog.warn("RTA order sync NOT started: service={}, enabled={}",
+                        rtaOrderSyncService != null, props.isRtaOrderSyncEnabled());
+            }
         }
 
         private void runConversionSync(LeaderElection.StopSignal jobStop) {
@@ -993,6 +1032,30 @@ public class AppConfiguration {
             taskLog.info("[AccountTagEventSync] task stopped");
         }
 
+        private void runRtaOrderSync(LeaderElection.StopSignal jobStop) {
+            Duration interval = props.getRtaOrderSyncInterval() == null
+                    ? Duration.ofMinutes(5)
+                    : props.getRtaOrderSyncInterval();
+            long intervalMs = interval.toMillis();
+            taskLog.info("[RtaOrderSync] task started, interval={}ms", intervalMs);
+            boolean firstRun = true;
+            while (!jobStop.isStopped()) {
+                syncRtaOrderOnce(firstRun ? "startup" : "periodic");
+                firstRun = false;
+                sleepResponsive(intervalMs, jobStop);
+            }
+            taskLog.info("[RtaOrderSync] task stopped");
+        }
+
+        private void syncRtaOrderOnce(String phase) {
+            try {
+                int n = rtaOrderSyncService.syncOnce();
+                taskLog.info("[RtaOrderSync] {} sync done: synced={}", phase, n);
+            } catch (Exception e) {
+                taskLog.error("[RtaOrderSync] {} sync error: {}", phase, e.getMessage(), e);
+            }
+        }
+
         private static void sleepResponsive(long ms, LeaderElection.StopSignal jobStop) {
             long deadline = System.currentTimeMillis() + ms;
             while (!jobStop.isStopped()) {

+ 20 - 29
src/main/java/com/adx/tencent/config/AppProperties.java

@@ -40,14 +40,13 @@ public class AppProperties {
     private int baiduTrackingMaxRedirects = 20;
     private int baiduTrackingMax500Retries = 3;
     private boolean rtaEnabled = false;
-    private String rtaEndpoint = "https://baidu.rta-gateway.com:8000/rta-proxy";
+    private String rtaEndpoint = "https://rta-gateway.baidu.com:443/rta-proxy";
     private Duration rtaTimeout = Duration.ofMillis(800);
-    private String rtaCustomerName;
-    private String rtaSspid;
-    private String rtaEKey;
     private String rtaTuPrefix = "huichuang1213";
     private String rtaOrderRedisPrefix = "adx:rta:order:";
-    private Duration rtaOrderCacheTtl = Duration.ofHours(24);
+    private boolean rtaOrderSyncEnabled = true;
+    private Duration rtaOrderSyncInterval = Duration.ofMinutes(5);
+    private Duration rtaOrderCacheTtl = Duration.ofMinutes(15);
 
     // Honor Callback
     private String honorConversionBaseUrl = "https://ads-drcn.platform.hihonorcloud.com";
@@ -356,30 +355,6 @@ public class AppProperties {
         this.rtaTimeout = rtaTimeout;
     }
 
-    public String getRtaCustomerName() {
-        return rtaCustomerName;
-    }
-
-    public void setRtaCustomerName(String rtaCustomerName) {
-        this.rtaCustomerName = rtaCustomerName;
-    }
-
-    public String getRtaSspid() {
-        return rtaSspid;
-    }
-
-    public void setRtaSspid(String rtaSspid) {
-        this.rtaSspid = rtaSspid;
-    }
-
-    public String getRtaEKey() {
-        return rtaEKey;
-    }
-
-    public void setRtaEKey(String rtaEKey) {
-        this.rtaEKey = rtaEKey;
-    }
-
     public String getRtaTuPrefix() {
         return rtaTuPrefix;
     }
@@ -404,6 +379,22 @@ public class AppProperties {
         this.rtaOrderCacheTtl = rtaOrderCacheTtl;
     }
 
+    public boolean isRtaOrderSyncEnabled() {
+        return rtaOrderSyncEnabled;
+    }
+
+    public void setRtaOrderSyncEnabled(boolean rtaOrderSyncEnabled) {
+        this.rtaOrderSyncEnabled = rtaOrderSyncEnabled;
+    }
+
+    public Duration getRtaOrderSyncInterval() {
+        return rtaOrderSyncInterval;
+    }
+
+    public void setRtaOrderSyncInterval(Duration rtaOrderSyncInterval) {
+        this.rtaOrderSyncInterval = rtaOrderSyncInterval;
+    }
+
     public String getHonorConversionBaseUrl() {
         return honorConversionBaseUrl;
     }

+ 26 - 9
src/main/java/com/adx/tencent/honor/controller/HonorTrackingController.java

@@ -142,14 +142,15 @@ public class HonorTrackingController {
         log.info("[荣耀][点击][入参] traceId={} ip={} params={}", traceId, clientIp(request), params);
 
         HonorBidRecord record = hotStore.findBidByMediaTrace(traceId);
+        BaiduRtaDecision rtaDecision = null;
         if (record == null) {
             HonorPlacement.Resolved resolved = placement.resolve(params);
-            BaiduRtaDecision rtaDecision = evaluateRta(traceId, resolved, params, request);
+            rtaDecision = evaluateRta(traceId, resolved, params, request);
             Map<String, String> rtaParams = enrichMediaParams(params, rtaDecision);
             if (!rtaDecision.allowsBid()) {
                 HonorBidRecord blocked = blockedBidRecord(traceId, "click", rtaParams, resolved, rtaDecision);
                 saveBlockedBid(blocked);
-                return Map.of("qk", blocked.getQk(), "traceId", traceId, "tagId", tagId, "rtaCode", safeInt(blocked.getRtaCode()));
+                return responseBody(params, blocked.getQk(), traceId, tagId, rtaDecision);
             }
             NormalizedBidResponse bidResponse = adxClient.requestBid("honor", traceId, buildBidRequest(request, traceId, params));
             if (bidResponse == null) {
@@ -188,7 +189,7 @@ public class HonorTrackingController {
             response.sendRedirect(destination);
             return null;
         }
-        return Map.of("qk", record.getQk(), "traceId", traceId, "tagId", tagId);
+        return responseBody(params, record.getQk(), traceId, tagId, rtaDecision);
     }
 
     private Map<String, Object> buildBidRequest(HttpServletRequest request, String traceId, Map<String, String> params) {
@@ -307,13 +308,8 @@ public class HonorTrackingController {
                 resolved.tagId(),
                 resolved.platform(),
                 params,
-                clientIp(request),
+                firstNonBlank(firstParam(params, "ip", "m6a"), clientIp(request)),
                 normalizeUserAgent(firstParam(params, "ua", "user_agent", "useragent"), resolved.platform()));
-        if (decision.isApplied()) {
-            log.info("[荣耀][RTA] traceId={} code={} matched={} requestId={} orderIds={} latencyMs={} error={}",
-                    traceId, decision.getCode(), decision.isMatched(), decision.getRequestId(),
-                    decision.getOrderIds(), decision.getLatencyMs(), decision.getErrorMessage());
-        }
         return decision;
     }
 
@@ -363,6 +359,27 @@ public class HonorTrackingController {
         return enriched;
     }
 
+    private static Map<String, Object> responseBody(Map<String, String> params,
+                                                    String qk,
+                                                    String traceId,
+                                                    String tagId,
+                                                    BaiduRtaDecision decision) {
+        Map<String, Object> body = new LinkedHashMap<>();
+        body.put("traceId", traceId);
+        body.put("qk", qk);
+        body.put("tagId", tagId);
+        if ("true".equalsIgnoreCase(firstNonBlank(params.get("_debugRta"), params.get("debugRta")))) {
+            body.put("rtaApplied", decision != null && decision.isApplied());
+            body.put("rtaMatched", decision == null ? null : decision.isMatched());
+            body.put("rtaCode", decision == null ? null : decision.getCode());
+            body.put("rtaRequestId", decision == null ? null : decision.getRequestId());
+            body.put("rtaRequestOrderId", decision == null ? null : decision.getRequestOrderIds());
+            body.put("rtaOrderId", decision == null ? null : decision.getOrderIds());
+            body.put("rtaError", decision == null ? null : decision.getErrorMessage());
+        }
+        return body;
+    }
+
     private static void putIfNotBlank(Map<String, String> params, String key, String value) {
         if (value != null && !value.isBlank()) {
             params.put(key, value);

+ 162 - 0
src/main/java/com/adx/tencent/httpapi/RtaClickSimulationController.java

@@ -0,0 +1,162 @@
+package com.adx.tencent.httpapi;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.server.ResponseStatusException;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import java.io.IOException;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * RTA 点击模拟入口。
+ * 只补齐媒体点击链路的必要参数,然后跳转到真实 /honor/click 或 /vivo/click。
+ */
+@RestController
+@RequestMapping("/admin/rta/simulate")
+public class RtaClickSimulationController {
+
+    @GetMapping("/click/{media}")
+    public void simulateClick(@PathVariable("media") String media,
+                              HttpServletRequest request,
+                              HttpServletResponse response) throws IOException {
+        String normalizedMedia = normalize(media);
+        if (!"honor".equals(normalizedMedia) && !"vivo".equals(normalizedMedia)) {
+            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "unsupported media: " + media);
+        }
+
+        Map<String, String> params = requestParams(request);
+        String tagId = firstParam(params, "tagId", "tag_id", "baidu_tag_id");
+        if (tagId == null) {
+            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "tagId is required");
+        }
+
+        String platform = firstParam(params, "platform", "os", "system", "device_os");
+        if (platform == null) {
+            platform = "android";
+        }
+
+        Map<String, String> simulated = new LinkedHashMap<>();
+        simulated.put("tagId", tagId);
+        simulated.put("platform", platform);
+        simulated.put("_debugRta", "true");
+        putIfNotBlank(simulated, "ip", firstParam(params, "ip", "m6a", "ipv4"));
+        putIfNotBlank(simulated, "ipv6", firstParam(params, "ipv6", "ipV6"));
+        putIfNotBlank(simulated, "ua", firstParam(params, "ua", "user_agent", "useragent"));
+        putIfNotBlank(simulated, "pkgName", firstParam(params, "pkgName", "pkg_name", "packageName"));
+        putIfNotBlank(simulated, "appList", firstParam(params, "appList", "app_list", "applist"));
+
+        if ("honor".equals(normalizedMedia)) {
+            buildHonorParams(params, simulated);
+        } else {
+            buildVivoParams(params, simulated);
+        }
+
+        UriComponentsBuilder builder = UriComponentsBuilder
+                .fromPath("/" + normalizedMedia + "/click");
+        simulated.forEach(builder::queryParam);
+        response.sendRedirect(builder.build().encode().toUriString());
+    }
+
+    private static void buildHonorParams(Map<String, String> params, Map<String, String> simulated) {
+        simulated.put("trackId", firstNonBlank(
+                firstParam(params, "trackId", "track_id"),
+                "sim-honor-click-" + shortId()));
+        putIfNotBlank(simulated, "advertiserId", firstParam(params, "advertiserId", "advertiser_id", "accountId", "accountid"));
+        putIfNotBlank(simulated, "requestId", firstParam(params, "requestId", "request_id", "REQUESTID"));
+        putIfNotBlank(simulated, "eventTime", firstNonBlank(firstParam(params, "eventTime", "time", "timestamp"), String.valueOf(System.currentTimeMillis())));
+        putDeviceParams(params, simulated);
+    }
+
+    private static void buildVivoParams(Map<String, String> params, Map<String, String> simulated) {
+        simulated.put("clickId", firstNonBlank(
+                firstParam(params, "clickId", "clickid", "click_id", "trace_id", "traceId"),
+                "sim-vivo-click-" + shortId()));
+        simulated.put("accountId", firstNonBlank(
+                firstParam(params, "accountId", "accountid", "advertiser_id", "advertiserId"),
+                "SIM_ACCOUNT_ID"));
+        simulated.put("eventSourceId", firstNonBlank(
+                firstParam(params, "eventSourceId", "event_source_id", "eventsourceid", "srcId", "src_id"),
+                "SIM_EVENT_SOURCE_ID"));
+        putDeviceParams(params, simulated);
+    }
+
+    private static void putDeviceParams(Map<String, String> params, Map<String, String> simulated) {
+        putIfNotBlank(simulated, "oaid", firstParam(params, "oaid", "oaid_plain"));
+        putIfNotBlank(simulated, "oaidMD5", firstParam(params, "oaidMD5", "oaid_md5", "oaidMd5", "hash_oaid"));
+        putIfNotBlank(simulated, "imei", firstParam(params, "imei", "imei_plain"));
+        putIfNotBlank(simulated, "imeiMD5", firstParam(params, "imeiMD5", "imei_md5", "imeiMd5"));
+        putIfNotBlank(simulated, "androidId", firstParam(params, "androidId", "androidid", "android_id"));
+        putIfNotBlank(simulated, "androidIdMD5", firstParam(params, "androidIdMD5", "android_id_md5", "androidIdMd5"));
+        putIfNotBlank(simulated, "idfa", firstParam(params, "idfa", "idfa_plain"));
+        putIfNotBlank(simulated, "idfaMD5", firstParam(params, "idfaMD5", "idfa_md5", "idfaMd5"));
+        putIfNotBlank(simulated, "caid", firstParam(params, "caid", "kenyid_caa", "kenyIdCaa"));
+        if (!hasAny(simulated, "oaid", "oaidMD5", "imei", "imeiMD5", "androidId", "androidIdMD5", "idfa", "idfaMD5", "caid")) {
+            simulated.put("oaid", "SIM_OAID_" + shortId());
+        }
+    }
+
+    private static Map<String, String> requestParams(HttpServletRequest request) {
+        Map<String, String> params = new LinkedHashMap<>();
+        request.getParameterMap().forEach((key, values) -> {
+            if (values != null && values.length > 0 && values[0] != null) {
+                params.put(key, values[0].trim());
+            }
+        });
+        return params;
+    }
+
+    private static void putIfNotBlank(Map<String, String> params, String key, String value) {
+        String normalized = normalize(value);
+        if (normalized != null) {
+            params.put(key, normalized);
+        }
+    }
+
+    private static boolean hasAny(Map<String, String> params, String... keys) {
+        for (String key : keys) {
+            if (normalize(params.get(key)) != null) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static String firstParam(Map<String, String> params, String... keys) {
+        for (String key : keys) {
+            String value = normalize(params.get(key));
+            if (value != null) {
+                return value;
+            }
+        }
+        return null;
+    }
+
+    private static String firstNonBlank(String... values) {
+        if (values == null) return null;
+        for (String value : values) {
+            String normalized = normalize(value);
+            if (normalized != null) {
+                return normalized;
+            }
+        }
+        return null;
+    }
+
+    private static String normalize(String value) {
+        if (value == null) return null;
+        String trimmed = value.trim();
+        return trimmed.isEmpty() ? null : trimmed;
+    }
+
+    private static String shortId() {
+        return UUID.randomUUID().toString().replace("-", "").substring(0, 12);
+    }
+}

+ 82 - 15
src/main/java/com/adx/tencent/httpapi/RtaDebugController.java

@@ -15,8 +15,13 @@ import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 import org.springframework.web.server.ResponseStatusException;
 
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
 import java.util.LinkedHashMap;
+import java.util.List;
 import java.util.Map;
+import java.util.Set;
+import java.util.StringJoiner;
 
 @RestController
 @RequestMapping("/admin/rta")
@@ -44,12 +49,14 @@ public class RtaDebugController {
         if (normalizedTagId == null) {
             throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "tagId is required");
         }
-        String orderId = normalize(orderResolver.resolveOrderId(normalizedTagId));
-        return Map.of(
-                "tagId", normalizedTagId,
-                "orderId", orderId,
-                "exists", orderId != null
-        );
+        List<String> orderIds = orderResolver.resolveOrderIds(normalizedTagId);
+        String orderId = joinOrderIds(orderIds);
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("tagId", normalizedTagId);
+        result.put("orderId", orderId);
+        result.put("orderIds", orderIds);
+        result.put("exists", !orderIds.isEmpty());
+        return result;
     }
 
     @PostMapping("/orders")
@@ -60,15 +67,21 @@ public class RtaDebugController {
         if (tagId == null) {
             throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "tagId is required");
         }
-        if (orderId == null) {
-            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "orderId is required");
+        List<String> orderIds = normalizeOrderIds(body == null ? null : body.getOrderIds());
+        if (orderId != null) {
+            orderIds = mergeOrderIds(orderIds, splitOrderIds(orderId));
         }
-        orderResolver.saveOrderId(tagId, orderId);
-        return Map.of(
-                "success", true,
-                "tagId", tagId,
-                "orderId", orderId
-        );
+        if (orderIds.isEmpty()) {
+            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "orderId/orderIds is required");
+        }
+        orderResolver.saveOrderIds(tagId, orderIds);
+        List<String> savedOrderIds = orderResolver.resolveOrderIds(tagId);
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("success", true);
+        result.put("tagId", tagId);
+        result.put("orderId", joinOrderIds(savedOrderIds));
+        result.put("orderIds", savedOrderIds);
+        return result;
     }
 
     @PostMapping("/evaluate")
@@ -104,8 +117,10 @@ public class RtaDebugController {
         result.put("resolvedAppId", resolved.appId());
         result.put("resolvedTagId", resolved.tagId());
         result.put("resolvedPlatform", resolved.platform());
-        String resolvedOrderId = orderResolver == null ? null : orderResolver.resolveOrderId(resolved.tagId());
+        List<String> resolvedOrderIds = orderResolver == null ? List.of() : orderResolver.resolveOrderIds(resolved.tagId());
+        String resolvedOrderId = joinOrderIds(resolvedOrderIds);
         result.put("resolvedOrderId", resolvedOrderId);
+        result.put("resolvedOrderIds", resolvedOrderIds);
         result.put("request", buildRequestPreview(resolved, resolvedOrderId, body, params));
         result.put("response", buildResponsePreview(decision));
         return result;
@@ -125,6 +140,49 @@ public class RtaDebugController {
         return trimmed.isEmpty() ? null : trimmed;
     }
 
+    private static List<String> splitOrderIds(String value) {
+        String normalized = normalize(value);
+        if (normalized == null) return List.of();
+        String[] parts = normalized.split(",");
+        List<String> result = new ArrayList<>();
+        for (String part : parts) {
+            String orderId = normalize(part);
+            if (orderId != null) {
+                result.add(orderId);
+            }
+        }
+        return normalizeOrderIds(result);
+    }
+
+    private static List<String> normalizeOrderIds(List<String> orderIds) {
+        if (orderIds == null || orderIds.isEmpty()) return List.of();
+        Set<String> deduped = new LinkedHashSet<>();
+        for (String orderId : orderIds) {
+            String normalized = normalize(orderId);
+            if (normalized != null) {
+                deduped.add(normalized);
+            }
+        }
+        return List.copyOf(deduped);
+    }
+
+    private static List<String> mergeOrderIds(List<String> first, List<String> second) {
+        Set<String> merged = new LinkedHashSet<>();
+        if (first != null) merged.addAll(first);
+        if (second != null) merged.addAll(second);
+        return List.copyOf(merged);
+    }
+
+    private static String joinOrderIds(List<String> orderIds) {
+        List<String> normalized = normalizeOrderIds(orderIds);
+        if (normalized.isEmpty()) return null;
+        StringJoiner joiner = new StringJoiner(",");
+        for (String orderId : normalized) {
+            joiner.add(orderId);
+        }
+        return joiner.toString();
+    }
+
     private PlacementResolved resolvePlacement(String media, Map<String, String> params) {
         return switch (media) {
             case "honor" -> resolveHonorPlacement(params);
@@ -199,6 +257,7 @@ public class RtaDebugController {
     public static class SaveOrderRequest {
         private String tagId;
         private String orderId;
+        private List<String> orderIds;
 
         public String getTagId() {
             return tagId;
@@ -215,6 +274,14 @@ public class RtaDebugController {
         public void setOrderId(String orderId) {
             this.orderId = orderId;
         }
+
+        public List<String> getOrderIds() {
+            return orderIds;
+        }
+
+        public void setOrderIds(List<String> orderIds) {
+            this.orderIds = orderIds;
+        }
     }
 
     public static class EvaluateRequest {

+ 67 - 26
src/main/java/com/adx/tencent/rta/BaiduRtaService.java

@@ -16,6 +16,7 @@ import java.net.http.HttpResponse;
 import java.nio.charset.StandardCharsets;
 import java.time.Duration;
 import java.time.Instant;
+import java.util.Arrays;
 import java.util.Base64;
 import java.util.LinkedHashMap;
 import java.util.Locale;
@@ -32,6 +33,7 @@ public class BaiduRtaService {
     private final String customerName;
     private final String sspid;
     private final String eKey;
+    private final String iKey;
     private final String tuPrefix;
     private final RtaOrderResolver orderResolver;
     private final HttpClient httpClient;
@@ -43,16 +45,19 @@ public class BaiduRtaService {
         this.enabled = props.isRtaEnabled();
         this.endpoint = props.getRtaEndpoint();
         this.timeout = props.getRtaTimeout() == null ? Duration.ofMillis(800) : props.getRtaTimeout();
-        String configuredCustomerName = trimToNull(props.getRtaCustomerName());
-        this.customerName = configuredCustomerName != null ? configuredCustomerName : trimToNull(props.getBaiduCustomerName());
-        String configuredSspid = trimToNull(props.getRtaSspid());
-        this.sspid = configuredSspid != null ? configuredSspid : (props.getBaiduMediaId() > 0 ? String.valueOf(props.getBaiduMediaId()) : null);
-        String configuredEKey = trimToNull(props.getRtaEKey());
-        this.eKey = configuredEKey != null ? configuredEKey : trimToNull(props.getBaiduAuctionPriceEKey());
+        this.customerName = trimToNull(props.getBaiduCustomerName());
+        this.sspid = props.getBaiduMediaId() > 0 ? String.valueOf(props.getBaiduMediaId()) : null;
+        this.eKey = trimToNull(props.getBaiduAuctionPriceEKey());
+        this.iKey = trimToNull(props.getBaiduAuctionPriceIKey());
         this.tuPrefix = trimToNull(props.getRtaTuPrefix());
         this.orderResolver = orderResolver;
         this.httpClient = HttpClient.newBuilder().connectTimeout(this.timeout).build();
         this.objectMapper = objectMapper;
+        log.info("[RTA][配置] enabled={} endpoint={} timeoutMs={} customerName={} sspid={} eKeyLength={} iKeyLength={} tuPrefix={}",
+                enabled, endpoint, timeout.toMillis(), customerName, sspid,
+                eKey == null ? 0 : eKey.getBytes(StandardCharsets.UTF_8).length,
+                iKey == null ? 0 : iKey.getBytes(StandardCharsets.UTF_8).length,
+                tuPrefix);
     }
 
     public BaiduRtaDecision evaluate(String media,
@@ -63,23 +68,32 @@ public class BaiduRtaService {
                                      String ip,
                                      String userAgent) {
         if (!enabled) {
+            log.info("[RTA][{}][绕过] reason=开关未启用 matched=true allowBid=true", mediaLabel(media));
             return BaiduRtaDecision.bypass();
         }
         if (!supportsMedia(media) || endpoint == null || endpoint.isBlank() || customerName == null || sspid == null || eKey == null) {
-            log.warn("[RTA][{}] skipped: missing global config", mediaLabel(media));
+            log.warn("[RTA][{}][绕过] reason=全局配置缺失 matched=true allowBid=true endpointConfigured={} customerNameConfigured={} sspidConfigured={} eKeyConfigured={}",
+                    mediaLabel(media), endpoint != null && !endpoint.isBlank(), customerName != null, sspid != null, eKey != null);
             return BaiduRtaDecision.bypass();
         }
         String tu = buildTu(tagId);
         String requestOrderId = trimToNull(orderResolver == null ? null : orderResolver.resolveOrderId(tagId));
         Map<String, Object> requestPreview = buildRequestPreview(appId, tagId, platform, params, ip, userAgent, tu, requestOrderId, null, null);
+        if (requestOrderId == null) {
+            log.info("[RTA][{}][绕过] reason=Redis未配置orderId matched=true allowBid=true tagId={} redisOnly=true",
+                    mediaLabel(media), tagId);
+            log.info("[RTA][{}][绕过预览] tagId={} request={}", mediaLabel(media), tagId, toJson(requestPreview));
+            return BaiduRtaDecision.bypass();
+        }
         if (tu == null || appId == null || appId.isBlank()) {
-            log.warn("[RTA][{}] skipped: missing tag config | tagId={} tu={} appId={}",
+            log.warn("[RTA][{}][绕过] reason=广告位配置缺失 matched=true allowBid=true tagId={} tu={} appId={}",
                     mediaLabel(media), tagId, tu, appId);
-            log.info("[RTA][{}][request-preview] {}", mediaLabel(media), toJson(requestPreview));
-            log.info("[RTA][{}][response-preview] {}", mediaLabel(media), toJson(Map.of(
+            log.info("[RTA][{}][绕过预览] tagId={} request={}", mediaLabel(media), tagId, toJson(requestPreview));
+            log.info("[RTA][{}][绕过预览] tagId={} response={}", mediaLabel(media), tagId, toJson(Map.of(
                     "applied", false,
                     "matched", true,
-                    "reason", "missing tag config",
+                    "allowBid", true,
+                    "reason", "广告位配置缺失",
                     "latencyMs", 0
             )));
             return BaiduRtaDecision.bypass();
@@ -94,7 +108,6 @@ public class BaiduRtaService {
 
         try {
             String requestBody = objectMapper.writeValueAsString(body);
-            log.info("[RTA][{}][request] {}", mediaLabel(media), requestBody);
             HttpRequest request = HttpRequest.newBuilder()
                     .uri(URI.create(endpoint))
                     .timeout(timeout)
@@ -103,22 +116,37 @@ public class BaiduRtaService {
                     .build();
             HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
             long latencyMs = (System.nanoTime() - startedAt) / 1_000_000L;
-            log.info("[RTA][{}][response] status={} body={}", mediaLabel(media), response.statusCode(), response.body());
+            String responseBody = response.body();
             if (response.statusCode() != 200) {
-                log.warn("[RTA][{}] httpStatus={} requestId={} body={}", mediaLabel(media), response.statusCode(), requestId, response.body());
+                log.info("[RTA][{}][未命中][请求] requestId={} body={}", mediaLabel(media), requestId, requestBody);
+                log.info("[RTA][{}][未命中][返回] requestId={} status={} headers={} body={}",
+                        mediaLabel(media), requestId, response.statusCode(), response.headers().map(), responseBody);
                 return BaiduRtaDecision.of(false, null, requestId, tu, requestOrderId, null, latencyMs, "httpStatus=" + response.statusCode());
             }
-            JsonNode root = objectMapper.readTree(response.body());
+            JsonNode root = objectMapper.readTree(responseBody);
             Integer code = root.hasNonNull("code") ? root.get("code").asInt() : null;
             String orderIds = root.hasNonNull("order_id") ? trimToNull(root.get("order_id").asText()) : null;
             boolean matched = code != null && code == 200 && orderIds != null && !orderIds.isBlank();
-            log.info("[RTA][{}] requestId={} code={} matched={} orderIds={} latencyMs={}",
-                    mediaLabel(media), requestId, code, matched, orderIds, latencyMs);
+            if (code != null && code == 4010 && latencyMs > 2_000L) {
+                log.warn("[RTA][{}][鉴权失败] latencyMs={},RTA timestamp 允许窗口是±2秒,请检查网络耗时以及当前进程是否加载了最新 timeout/sign 配置",
+                        mediaLabel(media), latencyMs);
+            }
+            if (!matched) {
+                log.info("[RTA][{}][未命中][请求] requestId={} body={}", mediaLabel(media), requestId, requestBody);
+                log.info("[RTA][{}][未命中][返回] requestId={} status={} headers={} body={}",
+                        mediaLabel(media), requestId, response.statusCode(), response.headers().map(), responseBody);
+            } else {
+                log.info("[RTA][{}][命中] requestId={} matched=true allowBid=true code={} requestOrderIds={} responseOrderIds={} latencyMs={} msg=命中,参与竞价",
+                        mediaLabel(media), requestId, code, requestOrderId, orderIds, latencyMs);
+            }
             return BaiduRtaDecision.of(matched, code, requestId, tu, requestOrderId, orderIds, latencyMs, null);
         } catch (Exception e) {
             long latencyMs = (System.nanoTime() - startedAt) / 1_000_000L;
-            log.warn("[RTA][{}] request failed | requestId={} error={}", mediaLabel(media), requestId, e.getMessage());
-            return BaiduRtaDecision.of(false, null, requestId, tu, requestOrderId, null, latencyMs, e.getMessage());
+            String error = e.getClass().getSimpleName() + ": " + (e.getMessage() == null ? "" : e.getMessage());
+            log.warn("[RTA][{}][异常] requestId={} matched=false allowBid=false code=null requestOrderIds={} responseOrderIds=null latencyMs={} errorType={} errorMessage={}",
+                    mediaLabel(media), requestId, requestOrderId, latencyMs, e.getClass().getName(), e.getMessage(), e);
+            log.info("[RTA][{}][未命中][请求] requestId={} body={}", mediaLabel(media), requestId, toJson(body));
+            return BaiduRtaDecision.of(false, null, requestId, tu, requestOrderId, null, latencyMs, error);
         }
     }
 
@@ -137,8 +165,6 @@ public class BaiduRtaService {
         body.put("customerName", customerName);
         body.put("sspid", sspid);
         body.put("appsid", trimToNull(appId));
-        body.put("tagId", trimToNull(tagId));
-        body.put("platform", trimToNull(platform));
         body.put("tu", tu);
         putIfNotBlank(body, "order_id", requestOrderId);
         body.put("os", os);
@@ -175,22 +201,37 @@ public class BaiduRtaService {
     }
 
     private String sign(String timestamp, String requestId) {
+        return signZeroPadding(eKey, timestamp, requestId);
+    }
+
+    private static String signZeroPadding(String keyValue, String timestamp, String requestId) {
         try {
-            byte[] rawKey = eKey.getBytes(StandardCharsets.UTF_8);
+            byte[] rawKey = keyValue.getBytes(StandardCharsets.UTF_8);
             if (rawKey.length < 16) {
                 throw new IllegalStateException("RTA eKey must contain at least 16 bytes");
             }
-            byte[] key = new byte[16];
-            System.arraycopy(rawKey, rawKey.length - 16, key, 0, 16);
-            Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
+            byte[] key = Arrays.copyOfRange(rawKey, rawKey.length - 16, rawKey.length);
+            byte[] payload = (timestamp + "\n" + requestId).getBytes(StandardCharsets.US_ASCII);
+            byte[] padded = zeroPad(payload, 16);
+            Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
             cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"));
-            byte[] encrypted = cipher.doFinal((timestamp + "\n" + requestId).getBytes(StandardCharsets.UTF_8));
+            byte[] encrypted = cipher.doFinal(padded);
             return Base64.getEncoder().encodeToString(encrypted);
         } catch (Exception e) {
             throw new RuntimeException("build RTA sign failed", e);
         }
     }
 
+    private static byte[] zeroPad(byte[] value, int blockSize) {
+        int remainder = value.length % blockSize;
+        if (remainder == 0) {
+            return value;
+        }
+        byte[] padded = new byte[value.length + blockSize - remainder];
+        System.arraycopy(value, 0, padded, 0, value.length);
+        return padded;
+    }
+
     private static int rtaOs(String platform) {
         String normalized = trimToNull(platform);
         if (normalized == null) return 0;

+ 70 - 24
src/main/java/com/adx/tencent/rta/RtaOrderResolver.java

@@ -1,7 +1,6 @@
 package com.adx.tencent.rta;
 
 import com.adx.tencent.config.AppProperties;
-import com.adx.tencent.rta.model.RtaTagOrderRecord;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.data.redis.core.StringRedisTemplate;
@@ -9,6 +8,11 @@ import org.springframework.lang.Nullable;
 import org.springframework.stereotype.Component;
 
 import java.time.Duration;
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.StringJoiner;
 
 @Component
 public class RtaOrderResolver {
@@ -29,43 +33,42 @@ public class RtaOrderResolver {
     }
 
     public String resolveOrderId(String tagId) {
+        return joinOrderIds(resolveOrderIds(tagId));
+    }
+
+    public List<String> resolveOrderIds(String tagId) {
         String normalizedTagId = normalize(tagId);
-        if (normalizedTagId == null) return null;
+        if (normalizedTagId == null) return List.of();
 
         try {
             String cached = normalize(redis.opsForValue().get(redisKey(normalizedTagId)));
             if (cached != null) {
-                return cached;
+                return splitOrderIds(cached);
             }
         } catch (Exception e) {
-            log.warn("[RTA][Order] read redis failed | tagId={} | error={}", normalizedTagId, e.getMessage());
-        }
-
-        if (store == null) {
-            return null;
+            log.warn("[RTA][Order][读取Redis失败] 广告位ID={} 错误={}", normalizedTagId, e.getMessage());
         }
+        return List.of();
+    }
 
-        try {
-            RtaTagOrderRecord record = store.getByTagId(normalizedTagId);
-            String orderId = record == null ? null : normalize(record.getOrderId());
-            if (orderId != null) {
-                cache(normalizedTagId, orderId);
-            }
-            return orderId;
-        } catch (Exception e) {
-            log.warn("[RTA][Order] read db failed | tagId={} | error={}", normalizedTagId, e.getMessage(), e);
-            return null;
+    public void saveOrderId(String tagId, String orderId) {
+        String normalizedTagId = normalize(tagId);
+        List<String> normalizedOrderIds = splitOrderIds(orderId);
+        if (normalizedTagId == null || normalizedOrderIds.isEmpty() || store == null) {
+            return;
         }
+        store.saveAll(normalizedTagId, normalizedOrderIds);
+        cache(normalizedTagId, joinOrderIds(mergeOrderIds(resolveOrderIds(normalizedTagId), normalizedOrderIds)));
     }
 
-    public void saveOrderId(String tagId, String orderId) {
+    public void saveOrderIds(String tagId, List<String> orderIds) {
         String normalizedTagId = normalize(tagId);
-        String normalizedOrderId = normalize(orderId);
-        if (normalizedTagId == null || normalizedOrderId == null || store == null) {
+        List<String> normalizedOrderIds = normalizeOrderIds(orderIds);
+        if (normalizedTagId == null || normalizedOrderIds.isEmpty() || store == null) {
             return;
         }
-        store.save(normalizedTagId, normalizedOrderId);
-        cache(normalizedTagId, normalizedOrderId);
+        store.saveAll(normalizedTagId, normalizedOrderIds);
+        cache(normalizedTagId, joinOrderIds(mergeOrderIds(resolveOrderIds(normalizedTagId), normalizedOrderIds)));
     }
 
     private void cache(String tagId, String orderId) {
@@ -76,7 +79,7 @@ public class RtaOrderResolver {
                 redis.opsForValue().set(redisKey(tagId), orderId);
             }
         } catch (Exception e) {
-            log.warn("[RTA][Order] write redis failed | tagId={} | error={}", tagId, e.getMessage());
+            log.warn("[RTA][Order][写入Redis失败] 广告位ID={} 错误={}", tagId, e.getMessage());
         }
     }
 
@@ -89,4 +92,47 @@ public class RtaOrderResolver {
         String trimmed = value.trim();
         return trimmed.isEmpty() ? null : trimmed;
     }
+
+    private static List<String> splitOrderIds(String value) {
+        String normalized = normalize(value);
+        if (normalized == null) return List.of();
+        String[] parts = normalized.split(",");
+        List<String> result = new ArrayList<>();
+        for (String part : parts) {
+            String orderId = normalize(part);
+            if (orderId != null) {
+                result.add(orderId);
+            }
+        }
+        return normalizeOrderIds(result);
+    }
+
+    private static List<String> normalizeOrderIds(List<String> orderIds) {
+        if (orderIds == null || orderIds.isEmpty()) return List.of();
+        Set<String> deduped = new LinkedHashSet<>();
+        for (String orderId : orderIds) {
+            String normalized = normalize(orderId);
+            if (normalized != null) {
+                deduped.add(normalized);
+            }
+        }
+        return List.copyOf(deduped);
+    }
+
+    private static List<String> mergeOrderIds(List<String> first, List<String> second) {
+        Set<String> merged = new LinkedHashSet<>();
+        if (first != null) merged.addAll(first);
+        if (second != null) merged.addAll(second);
+        return List.copyOf(merged);
+    }
+
+    private static String joinOrderIds(List<String> orderIds) {
+        List<String> normalized = normalizeOrderIds(orderIds);
+        if (normalized.isEmpty()) return null;
+        StringJoiner joiner = new StringJoiner(",");
+        for (String orderId : normalized) {
+            joiner.add(orderId);
+        }
+        return joiner.toString();
+    }
 }

+ 68 - 12
src/main/java/com/adx/tencent/rta/RtaOrderStore.java

@@ -5,10 +5,17 @@ import com.adx.tencent.rta.model.RtaTagOrderRecord;
 import org.apache.ibatis.session.SqlSession;
 import org.apache.ibatis.session.SqlSessionFactory;
 
+import java.sql.DatabaseMetaData;
+import java.sql.ResultSet;
 import java.sql.Statement;
 import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
 
 public class RtaOrderStore {
+    private static final String TABLE = "rta_tag_orders";
+    private static final String MIGRATION_TABLE = "rta_tag_orders_migrating";
+
     private final SqlSessionFactory sqlSessionFactory;
 
     public RtaOrderStore(SqlSessionFactory sqlSessionFactory) {
@@ -16,30 +23,72 @@ public class RtaOrderStore {
     }
 
     public void migrate() {
-        String ddl = """
-                CREATE TABLE IF NOT EXISTS rta_tag_orders (
-                    tag_id VARCHAR(128) NOT NULL COMMENT '百度广告位ID',
-                    order_id VARCHAR(255) NOT NULL COMMENT 'RTA order_id',
-                    updated_at TIMESTAMP(3) NOT NULL COMMENT '更新时间',
-                    PRIMARY KEY (tag_id)
-                ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='RTA广告位order_id映射'
-                """;
         try (SqlSession session = sqlSessionFactory.openSession(true)) {
             Statement stmt = session.getConnection().createStatement();
-            stmt.execute(ddl);
+            stmt.execute(createTableDdl(TABLE));
+            migratePrimaryKeyIfNeeded(session, stmt);
             stmt.close();
         } catch (Exception e) {
             throw new RuntimeException("rta_tag_orders migrate failed", e);
         }
     }
 
-    public RtaTagOrderRecord getByTagId(String tagId) {
-        if (tagId == null || tagId.isBlank()) return null;
+    private String createTableDdl(String tableName) {
+        return """
+                CREATE TABLE IF NOT EXISTS %s (
+                    tag_id VARCHAR(128) NOT NULL COMMENT '百度广告位ID',
+                    order_id VARCHAR(255) NOT NULL COMMENT 'RTA order_id',
+                    updated_at TIMESTAMP(3) NOT NULL COMMENT '更新时间',
+                    PRIMARY KEY (tag_id, order_id),
+                    KEY idx_rta_tag_orders_updated_at (updated_at)
+                ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='RTA广告位order_id映射'
+                """.formatted(tableName);
+    }
+
+    private void migratePrimaryKeyIfNeeded(SqlSession session, Statement stmt) throws Exception {
+        List<String> primaryKeys = new ArrayList<>();
+        DatabaseMetaData metaData = session.getConnection().getMetaData();
+        try (ResultSet rs = metaData.getPrimaryKeys(null, null, TABLE)) {
+            while (rs.next()) {
+                primaryKeys.add(rs.getString("COLUMN_NAME"));
+            }
+        }
+        if (primaryKeys.size() == 1 && "tag_id".equals(primaryKeys.get(0))) {
+            rebuildTableWithCompositePrimaryKey(stmt);
+        }
+        try {
+            stmt.execute("ALTER TABLE " + TABLE + " ADD INDEX idx_rta_tag_orders_updated_at (updated_at)");
+        } catch (Exception ignored) {
+        }
+    }
+
+    private void rebuildTableWithCompositePrimaryKey(Statement stmt) throws Exception {
+        String backupTable = TABLE + "_backup_" + Instant.now().toEpochMilli();
+        stmt.execute("DROP TABLE IF EXISTS " + MIGRATION_TABLE);
+        stmt.execute(createTableDdl(MIGRATION_TABLE));
+        stmt.execute("""
+                INSERT IGNORE INTO rta_tag_orders_migrating (tag_id, order_id, updated_at)
+                SELECT TRIM(tag_id), TRIM(order_id), COALESCE(updated_at, CURRENT_TIMESTAMP(3))
+                FROM rta_tag_orders
+                WHERE tag_id IS NOT NULL AND TRIM(tag_id) <> ''
+                  AND order_id IS NOT NULL AND TRIM(order_id) <> ''
+                """);
+        stmt.execute("RENAME TABLE " + TABLE + " TO " + backupTable + ", " + MIGRATION_TABLE + " TO " + TABLE);
+    }
+
+    public List<RtaTagOrderRecord> listByTagId(String tagId) {
+        if (tagId == null || tagId.isBlank()) return List.of();
         try (SqlSession session = sqlSessionFactory.openSession(true)) {
             return session.getMapper(RtaTagOrderMapper.class).selectByTagId(tagId.trim());
         }
     }
 
+    public List<RtaTagOrderRecord> listAll() {
+        try (SqlSession session = sqlSessionFactory.openSession(true)) {
+            return session.getMapper(RtaTagOrderMapper.class).selectAll();
+        }
+    }
+
     public void save(String tagId, String orderId) {
         if (tagId == null || tagId.isBlank() || orderId == null || orderId.isBlank()) return;
         RtaTagOrderRecord record = new RtaTagOrderRecord();
@@ -47,7 +96,14 @@ public class RtaOrderStore {
         record.setOrderId(orderId.trim());
         record.setUpdatedAt(Instant.now());
         try (SqlSession session = sqlSessionFactory.openSession(true)) {
-            session.getMapper(RtaTagOrderMapper.class).insertOrUpdate(record);
+            session.getMapper(RtaTagOrderMapper.class).insert(record);
+        }
+    }
+
+    public void saveAll(String tagId, List<String> orderIds) {
+        if (tagId == null || tagId.isBlank() || orderIds == null || orderIds.isEmpty()) return;
+        for (String orderId : orderIds) {
+            save(tagId, orderId);
         }
     }
 }

+ 93 - 0
src/main/java/com/adx/tencent/rta/RtaOrderSyncService.java

@@ -0,0 +1,93 @@
+package com.adx.tencent.rta;
+
+import com.adx.tencent.rta.model.RtaTagOrderRecord;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.data.redis.connection.RedisConnection;
+import org.springframework.data.redis.connection.RedisStringCommands;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.data.redis.core.types.Expiration;
+
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * RTA order_id 同步服务。
+ * 定时从 rta_tag_orders 表读取全部记录,同步到 Redis:
+ *   key   = {prefix}{tag_id}
+ *   value = order_id 列表(逗号分隔)
+ */
+public class RtaOrderSyncService {
+
+    private static final Logger log = LoggerFactory.getLogger(RtaOrderSyncService.class);
+
+    private final RtaOrderStore store;
+    private final StringRedisTemplate redis;
+    private final String keyPrefix;
+    private final Duration ttl;
+
+    public RtaOrderSyncService(RtaOrderStore store,
+                               StringRedisTemplate redis,
+                               String keyPrefix,
+                               Duration ttl) {
+        this.store = store;
+        this.redis = redis;
+        this.keyPrefix = keyPrefix == null ? "adx:rta:order:" : keyPrefix;
+        this.ttl = ttl;
+    }
+
+    public int syncOnce() {
+        List<RtaTagOrderRecord> rows = store.listAll();
+        if (rows == null || rows.isEmpty()) {
+            log.info("[RtaOrderSync] 无数据可同步");
+            return 0;
+        }
+
+        Map<String, Set<String>> grouped = new LinkedHashMap<>();
+        for (RtaTagOrderRecord row : rows) {
+            if (row.getTagId() == null || row.getTagId().isBlank()) {
+                continue;
+            }
+            if (row.getOrderId() == null || row.getOrderId().isBlank()) {
+                continue;
+            }
+            grouped.computeIfAbsent(row.getTagId().trim(), ignored -> new LinkedHashSet<>())
+                    .add(row.getOrderId().trim());
+        }
+        if (grouped.isEmpty()) {
+            log.info("[RtaOrderSync] 无有效数据可同步");
+            return 0;
+        }
+
+        long ttlMs = (ttl != null && !ttl.isZero() && !ttl.isNegative()) ? ttl.toMillis() : 0;
+        redis.executePipelined((RedisConnection conn) -> {
+            for (Map.Entry<String, Set<String>> entry : grouped.entrySet()) {
+                byte[] key = redisKey(entry.getKey()).getBytes(StandardCharsets.UTF_8);
+                String value = entry.getValue().stream().collect(Collectors.joining(","));
+                byte[] val = value.getBytes(StandardCharsets.UTF_8);
+                if (ttlMs > 0) {
+                    conn.stringCommands().set(key, val,
+                            Expiration.milliseconds(ttlMs),
+                            RedisStringCommands.SetOption.UPSERT);
+                } else {
+                    conn.stringCommands().set(key, val);
+                }
+            }
+            return null;
+        });
+
+        log.info("[RtaOrderSync] 同步完成,共 {} 条 RTA order_id 写入 Redis, 聚合后 {} 个广告位",
+                rows.size(), grouped.size());
+        return rows.size();
+    }
+
+    private String redisKey(String tagId) {
+        return keyPrefix + tagId;
+    }
+}

+ 6 - 2
src/main/java/com/adx/tencent/rta/mapper/RtaTagOrderMapper.java

@@ -4,9 +4,13 @@ import com.adx.tencent.rta.model.RtaTagOrderRecord;
 import org.apache.ibatis.annotations.Mapper;
 import org.apache.ibatis.annotations.Param;
 
+import java.util.List;
+
 @Mapper
 public interface RtaTagOrderMapper {
-    RtaTagOrderRecord selectByTagId(@Param("tagId") String tagId);
+    List<RtaTagOrderRecord> selectAll();
+
+    List<RtaTagOrderRecord> selectByTagId(@Param("tagId") String tagId);
 
-    void insertOrUpdate(@Param("record") RtaTagOrderRecord record);
+    void insert(@Param("record") RtaTagOrderRecord record);
 }

+ 26 - 10
src/main/java/com/adx/tencent/vivo/controller/VivoTrackingController.java

@@ -190,17 +190,17 @@ public class VivoTrackingController {
             throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "missing vivo trace id (clickId)");
         }
         requireVivoParams(params);
-        log.info("[Vivo][点击][入参] traceId={} ip={} params={}", traceId, clientIp(request), params);
 
         VivoBidRecord record = hotStore.findBidByMediaTrace(traceId);
+        BaiduRtaDecision rtaDecision = null;
         if (record == null) {
             VivoPlacement.Resolved resolved = placement.resolve(params);
-            BaiduRtaDecision rtaDecision = evaluateRta(traceId, resolved, params, request);
+            rtaDecision = evaluateRta(traceId, resolved, params, request);
             Map<String, String> rtaParams = enrichMediaParams(params, rtaDecision);
             if (!rtaDecision.allowsBid()) {
                 VivoBidRecord blocked = blockedBidRecord(traceId, "click", rtaParams, resolved, rtaDecision);
                 saveBlockedBid(blocked);
-                return Map.of("qk", blocked.getQk(), "traceId", traceId, "tagId", blocked.getTagId(), "rtaCode", safeInt(blocked.getRtaCode()));
+                return responseBody(params, blocked.getQk(), traceId, blocked.getTagId(), rtaDecision);
             }
             NormalizedBidResponse bidResponse = adxClient.requestBid("vivo", traceId, buildBidRequest(request, traceId, params));
             if (bidResponse == null) {
@@ -240,7 +240,7 @@ public class VivoTrackingController {
             response.sendRedirect(destination);
             return null;
         }
-        return Map.of("qk", record.getQk(), "traceId", traceId, "tagId", record.getTagId());
+        return responseBody(params, record.getQk(), traceId, record.getTagId(), rtaDecision);
     }
 
     private void ensureReady() {
@@ -382,13 +382,8 @@ public class VivoTrackingController {
                 resolved.tagId(),
                 resolved.platform(),
                 params,
-                clientIp(request),
+                firstNonBlank(firstParam(params, "ip"), clientIp(request)),
                 normalizeUserAgent(firstParam(params, "ua"), resolved.platform()));
-        if (decision.isApplied()) {
-            log.info("[Vivo][RTA] traceId={} code={} matched={} requestId={} orderIds={} latencyMs={} error={}",
-                    traceId, decision.getCode(), decision.isMatched(), decision.getRequestId(),
-                    decision.getOrderIds(), decision.getLatencyMs(), decision.getErrorMessage());
-        }
         return decision;
     }
 
@@ -438,6 +433,27 @@ public class VivoTrackingController {
         return enriched;
     }
 
+    private static Map<String, Object> responseBody(Map<String, String> params,
+                                                    String qk,
+                                                    String traceId,
+                                                    String tagId,
+                                                    BaiduRtaDecision decision) {
+        Map<String, Object> body = new LinkedHashMap<>();
+        body.put("traceId", traceId);
+        body.put("qk", qk);
+        body.put("tagId", tagId);
+        if ("true".equalsIgnoreCase(firstNonBlank(params.get("_debugRta"), params.get("debugRta")))) {
+            body.put("rtaApplied", decision != null && decision.isApplied());
+            body.put("rtaMatched", decision == null ? null : decision.isMatched());
+            body.put("rtaCode", decision == null ? null : decision.getCode());
+            body.put("rtaRequestId", decision == null ? null : decision.getRequestId());
+            body.put("rtaRequestOrderId", decision == null ? null : decision.getRequestOrderIds());
+            body.put("rtaOrderId", decision == null ? null : decision.getOrderIds());
+            body.put("rtaError", decision == null ? null : decision.getErrorMessage());
+        }
+        return body;
+    }
+
     private static void putIfNotBlank(Map<String, String> params, String key, String value) {
         if (value != null && !value.isBlank()) {
             params.put(key, value);

+ 6 - 8
src/main/resources/application.yml

@@ -25,15 +25,13 @@ adx:
   baidu-android-tag-ids: jlt52,jlt53,jlt54,jlt55,jlt56,jlt57,jlt58,jlt59,jlt60,jlt61,jlt62,jlt63,jlt64,jlt65,jlt66,jlt67,jlt68,jlt69,jlt70,jlt71,jlt72,jlt73,jlt74,jlt75,jlt76,jlt77,jlt78,jlt79,jlt80,jlt81,jlt82,jlt83,jlt84,jlt85,jlt86,jlt87,jlt88,jlt89,jlt90,jlt91,jlt92,jlt93,jlt94,jlt95,jlt96,jlt97,jlt98,jlt99,jlt100,jlt101
   baidu-ios-app-id: efff0562
   baidu-ios-tag-ids: jlt01,jlt03,jlt04,jlt05,jlt06,jlt07,jlt08,jlt09,jlt10,jlt11,jlt12,jlt13,jlt14,jlt15,jlt16,jlt17,jlt18,jlt19,jlt20,jlt21,jlt22,jlt23,jlt24,jlt25,jlt26,jlt27,jlt28,jlt29,jlt30,jlt31,jlt32,jlt33,jlt34,jlt35,jlt36,jlt37,jlt38,jlt39,jlt40,jlt41,jlt42,jlt43,jlt44,jlt45,jlt46,jlt47,jlt48,jlt49,jlt50,jlt51
-  rta-enabled: false
-  rta-endpoint: "https://baidu.rta-gateway.com:8000/rta-proxy"
-  rta-timeout: 800ms
-  rta-customer-name:
-  rta-sspid:
-  rta-e-key:
+  rta-enabled: true
+  rta-endpoint: "https://rta-gateway.baidu.com:443/rta-proxy"
+  rta-timeout: 8000ms
   rta-tu-prefix: "huichuang1213"
-  honor-rta-order-ids: []
-  vivo-rta-order-ids: []
+  rta-order-sync-enabled: true
+  rta-order-sync-interval: 5m
+  rta-order-cache-ttl: 15m
 
 baidu:
   clue:

+ 8 - 3
src/main/resources/mapper/RtaTagOrderMapper.xml

@@ -8,21 +8,26 @@
         <result property="updatedAt" column="updated_at"/>
     </resultMap>
 
+    <select id="selectAll" resultMap="rtaTagOrderResultMap">
+        SELECT tag_id, order_id, updated_at
+        FROM rta_tag_orders
+        ORDER BY tag_id ASC, updated_at DESC, order_id ASC
+    </select>
+
     <select id="selectByTagId" resultMap="rtaTagOrderResultMap">
         SELECT tag_id, order_id, updated_at
         FROM rta_tag_orders
         WHERE tag_id = #{tagId}
-        LIMIT 1
+        ORDER BY updated_at DESC, order_id ASC
     </select>
 
-    <insert id="insertOrUpdate">
+    <insert id="insert">
         INSERT INTO rta_tag_orders (
             tag_id, order_id, updated_at
         ) VALUES (
             #{record.tagId}, #{record.orderId}, #{record.updatedAtTs}
         )
         ON DUPLICATE KEY UPDATE
-            order_id = VALUES(order_id),
             updated_at = VALUES(updated_at)
     </insert>
 </mapper>

+ 13 - 0
src/main/resources/schema-honor.sql

@@ -81,12 +81,24 @@ CREATE TABLE IF NOT EXISTS honor_tag_event (
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='荣耀广告位转化事件映射';
 
 -- ------------------------------------------------------------
+-- 5. RTA 广告位 order_id 配置表
+-- ------------------------------------------------------------
+CREATE TABLE IF NOT EXISTS rta_tag_orders (
+    tag_id VARCHAR(128) NOT NULL COMMENT '百度广告位ID',
+    order_id VARCHAR(255) NOT NULL COMMENT 'RTA order_id',
+    updated_at TIMESTAMP(3) NOT NULL COMMENT '更新时间',
+    PRIMARY KEY (tag_id, order_id),
+    KEY idx_rta_tag_orders_updated_at (updated_at)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='RTA广告位order_id映射';
+
+-- ------------------------------------------------------------
 -- 初始化说明
 -- ------------------------------------------------------------
 -- 1. 对外提供的荣耀监测链接必须显式带上 tagId。
 -- 2. 百度转化明细统一落 tencent_baidu_conversions,使用 media=honor 区分,tag_id 可为空。
 -- 3. honor_tag_event 需要按广告位配置 tag_id + baidu_act -> conversion_id 映射。
+-- 4. rta_tag_orders 支持同一个 tag_id 配置多个 order_id,RTA 请求会以逗号分隔传给百度。
+-- 5. Redis 热存储命名空间为:
 --    adx:honor:bid:{qk}
 --    adx:honor:media:honor:{traceId}
 --    adx:honor:events

+ 8 - 0
src/main/resources/schema-vivo.sql

@@ -64,6 +64,14 @@ CREATE TABLE IF NOT EXISTS vivo_tag_event (
     PRIMARY KEY (tag_id, baidu_act, event_type)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='vivo广告位回传事件配置';
 
+CREATE TABLE IF NOT EXISTS rta_tag_orders (
+    tag_id VARCHAR(128) NOT NULL COMMENT '百度广告位ID',
+    order_id VARCHAR(255) NOT NULL COMMENT 'RTA order_id',
+    updated_at TIMESTAMP(3) NOT NULL COMMENT '更新时间',
+    PRIMARY KEY (tag_id, order_id),
+    KEY idx_rta_tag_orders_updated_at (updated_at)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='RTA广告位order_id映射';
+
 CREATE TABLE IF NOT EXISTS vivo_account_tokens (
     account_id VARCHAR(128) NOT NULL COMMENT 'vivo 二代账户ID',
     account_name VARCHAR(255) COMMENT 'vivo 二代账户名称',

+ 14 - 0
src/main/resources/schema.sql

@@ -121,6 +121,17 @@ CREATE TABLE IF NOT EXISTS tencent_account_tag_event (
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='腾讯账户级广告位回传方式配置';
 
 -- ------------------------------------------------------------
+-- 7. RTA 广告位 order_id 配置表(荣耀/vivo 共用)
+-- ------------------------------------------------------------
+CREATE TABLE IF NOT EXISTS rta_tag_orders (
+    tag_id VARCHAR(128) NOT NULL COMMENT '百度广告位ID',
+    order_id VARCHAR(255) NOT NULL COMMENT 'RTA order_id',
+    updated_at TIMESTAMP(3) NOT NULL COMMENT '更新时间',
+    PRIMARY KEY (tag_id, order_id),
+    KEY idx_rta_tag_orders_updated_at (updated_at)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='RTA广告位order_id映射';
+
+-- ------------------------------------------------------------
 -- 字段补丁(兼容旧表升级)
 -- ------------------------------------------------------------
 -- 如果 req_id 原来是 VARCHAR(128),扩展为 512
@@ -138,6 +149,9 @@ CREATE TABLE IF NOT EXISTS tencent_account_tag_event (
 -- ALTER TABLE tencent_baidu_conversions ADD COLUMN tag_id VARCHAR(128) AFTER media;
 -- ALTER TABLE tencent_baidu_conversions ADD KEY idx_tencent_baidu_conversions_tag_id (tag_id);
 
+-- 如果旧 rta_tag_orders 还是单广告位单 order_id 主键,TiDB clustered index 不支持 DROP PRIMARY KEY。
+-- 启动迁移会自动创建 rta_tag_orders_migrating,复制数据后 RENAME TABLE 切换,旧表保留为 rta_tag_orders_backup_{timestamp}。
+
 -- 如果旧表没有 request_body / dispatch_status 字段
 -- ALTER TABLE tencent_media_callbacks ADD COLUMN request_body TEXT AFTER error_message;
 -- ALTER TABLE tencent_media_callbacks ADD COLUMN dispatch_status VARCHAR(32) NOT NULL DEFAULT 'SENT' AFTER request_body;