yumeng před 3 týdny
rodič
revize
4ea2b1cdaa

+ 17 - 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.RtaOrderStore;
 import com.adx.tencent.tagsync.TagEventResolver;
 import com.adx.tencent.tagsync.TagEventSyncService;
 import com.adx.tencent.tencent.TencentClient;
@@ -187,6 +188,22 @@ public class AppConfiguration {
         }
     }
 
+    @Bean
+    public RtaOrderStore rtaOrderStore(@Nullable DataSource tidbDataSource) {
+        if (tidbDataSource == null) {
+            return null;
+        }
+        try {
+            SqlSessionFactory sqlSessionFactory = createSqlSessionFactory(tidbDataSource);
+            RtaOrderStore store = new RtaOrderStore(sqlSessionFactory);
+            store.migrate();
+            return store;
+        } catch (Exception e) {
+            log.warn("Failed to open RTA order store: {}", e.getMessage(), e);
+            return null;
+        }
+    }
+
     private SqlSessionFactory createSqlSessionFactory(DataSource dataSource) throws Exception {
         SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
         factoryBean.setDataSource(dataSource);

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

@@ -46,8 +46,8 @@ public class AppProperties {
     private String rtaSspid;
     private String rtaEKey;
     private String rtaTuPrefix = "huichuang1213";
-    private List<String> honorRtaOrderIds = new ArrayList<>();
-    private List<String> vivoRtaOrderIds = new ArrayList<>();
+    private String rtaOrderRedisPrefix = "adx:rta:order:";
+    private Duration rtaOrderCacheTtl = Duration.ofHours(24);
 
     // Honor Callback
     private String honorConversionBaseUrl = "https://ads-drcn.platform.hihonorcloud.com";
@@ -381,24 +381,24 @@ public class AppProperties {
         return rtaTuPrefix;
     }
 
-    public List<String> getHonorRtaOrderIds() {
-        return honorRtaOrderIds;
+    public void setRtaTuPrefix(String rtaTuPrefix) {
+        this.rtaTuPrefix = rtaTuPrefix;
     }
 
-    public void setHonorRtaOrderIds(List<String> honorRtaOrderIds) {
-        this.honorRtaOrderIds = honorRtaOrderIds;
+    public String getRtaOrderRedisPrefix() {
+        return rtaOrderRedisPrefix;
     }
 
-    public void setRtaTuPrefix(String rtaTuPrefix) {
-        this.rtaTuPrefix = rtaTuPrefix;
+    public void setRtaOrderRedisPrefix(String rtaOrderRedisPrefix) {
+        this.rtaOrderRedisPrefix = rtaOrderRedisPrefix;
     }
 
-    public List<String> getVivoRtaOrderIds() {
-        return vivoRtaOrderIds;
+    public Duration getRtaOrderCacheTtl() {
+        return rtaOrderCacheTtl;
     }
 
-    public void setVivoRtaOrderIds(List<String> vivoRtaOrderIds) {
-        this.vivoRtaOrderIds = vivoRtaOrderIds;
+    public void setRtaOrderCacheTtl(Duration rtaOrderCacheTtl) {
+        this.rtaOrderCacheTtl = rtaOrderCacheTtl;
     }
 
     public String getHonorConversionBaseUrl() {

+ 276 - 0
src/main/java/com/adx/tencent/httpapi/RtaDebugController.java

@@ -0,0 +1,276 @@
+package com.adx.tencent.httpapi;
+
+import com.adx.tencent.honor.service.HonorPlacement;
+import com.adx.tencent.rta.BaiduRtaDecision;
+import com.adx.tencent.rta.BaiduRtaService;
+import com.adx.tencent.rta.RtaOrderResolver;
+import com.adx.tencent.vivo.service.VivoPlacement;
+import org.springframework.http.HttpStatus;
+import org.springframework.lang.Nullable;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.server.ResponseStatusException;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/admin/rta")
+public class RtaDebugController {
+
+    private final BaiduRtaService rtaService;
+    private final RtaOrderResolver orderResolver;
+    private final HonorPlacement honorPlacement;
+    private final VivoPlacement vivoPlacement;
+
+    public RtaDebugController(BaiduRtaService rtaService,
+                              @Nullable RtaOrderResolver orderResolver,
+                              @Nullable HonorPlacement honorPlacement,
+                              @Nullable VivoPlacement vivoPlacement) {
+        this.rtaService = rtaService;
+        this.orderResolver = orderResolver;
+        this.honorPlacement = honorPlacement;
+        this.vivoPlacement = vivoPlacement;
+    }
+
+    @GetMapping("/orders/{tagId}")
+    public Object getOrder(@PathVariable("tagId") String tagId) {
+        ensureResolver();
+        String normalizedTagId = normalize(tagId);
+        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
+        );
+    }
+
+    @PostMapping("/orders")
+    public Object saveOrder(@RequestBody SaveOrderRequest body) {
+        ensureResolver();
+        String tagId = normalize(body == null ? null : body.getTagId());
+        String orderId = normalize(body == null ? null : body.getOrderId());
+        if (tagId == null) {
+            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "tagId is required");
+        }
+        if (orderId == null) {
+            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "orderId is required");
+        }
+        orderResolver.saveOrderId(tagId, orderId);
+        return Map.of(
+                "success", true,
+                "tagId", tagId,
+                "orderId", orderId
+        );
+    }
+
+    @PostMapping("/evaluate")
+    public Object evaluate(@RequestBody EvaluateRequest body) {
+        if (body == null) {
+            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "request body is required");
+        }
+        String media = normalize(body.getMedia());
+        if (media == null) {
+            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "media is required");
+        }
+
+        Map<String, String> params = new LinkedHashMap<>();
+        if (body.getParams() != null) {
+            params.putAll(body.getParams());
+        }
+        putIfNotBlank(params, "tagId", body.getTagId());
+        putIfNotBlank(params, "platform", body.getPlatform());
+
+        PlacementResolved resolved = resolvePlacement(media, params);
+        BaiduRtaDecision decision = rtaService.evaluate(
+                media,
+                resolved.appId(),
+                resolved.tagId(),
+                resolved.platform(),
+                params,
+                body.getIp(),
+                body.getUserAgent()
+        );
+
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("media", media);
+        result.put("resolvedAppId", resolved.appId());
+        result.put("resolvedTagId", resolved.tagId());
+        result.put("resolvedPlatform", resolved.platform());
+        String resolvedOrderId = orderResolver == null ? null : orderResolver.resolveOrderId(resolved.tagId());
+        result.put("resolvedOrderId", resolvedOrderId);
+        result.put("request", buildRequestPreview(resolved, resolvedOrderId, body, params));
+        result.put("response", buildResponsePreview(decision));
+        return result;
+    }
+
+    private void ensureResolver() {
+        if (orderResolver == null) {
+            throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "rta order resolver is not configured");
+        }
+    }
+
+    private static String normalize(String value) {
+        if (value == null) {
+            return null;
+        }
+        String trimmed = value.trim();
+        return trimmed.isEmpty() ? null : trimmed;
+    }
+
+    private PlacementResolved resolvePlacement(String media, Map<String, String> params) {
+        return switch (media) {
+            case "honor" -> resolveHonorPlacement(params);
+            case "vivo" -> resolveVivoPlacement(params);
+            default -> throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "unsupported media: " + media);
+        };
+    }
+
+    private PlacementResolved resolveHonorPlacement(Map<String, String> params) {
+        if (honorPlacement == null) {
+            throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "honor placement is not configured");
+        }
+        try {
+            HonorPlacement.Resolved resolved = honorPlacement.resolve(params);
+            return new PlacementResolved(resolved.platform(), resolved.appId(), resolved.tagId());
+        } catch (IllegalArgumentException e) {
+            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage());
+        }
+    }
+
+    private PlacementResolved resolveVivoPlacement(Map<String, String> params) {
+        if (vivoPlacement == null) {
+            throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "vivo placement is not configured");
+        }
+        try {
+            VivoPlacement.Resolved resolved = vivoPlacement.resolve(params);
+            return new PlacementResolved(resolved.platform(), resolved.appId(), resolved.tagId());
+        } catch (IllegalArgumentException e) {
+            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage());
+        }
+    }
+
+    private static void putIfNotBlank(Map<String, String> params, String key, String value) {
+        String normalized = normalize(value);
+        if (normalized != null) {
+            params.put(key, normalized);
+        }
+    }
+
+    private Map<String, Object> buildRequestPreview(PlacementResolved resolved,
+                                                    String resolvedOrderId,
+                                                    EvaluateRequest body,
+                                                    Map<String, String> params) {
+        Map<String, Object> request = new LinkedHashMap<>();
+        request.put("appId", resolved.appId());
+        request.put("tagId", resolved.tagId());
+        request.put("platform", resolved.platform());
+        request.put("orderId", resolvedOrderId);
+        request.put("ip", normalize(body.getIp()));
+        request.put("userAgent", normalize(body.getUserAgent()));
+        request.put("params", params);
+        return request;
+    }
+
+    private Map<String, Object> buildResponsePreview(BaiduRtaDecision decision) {
+        Map<String, Object> response = new LinkedHashMap<>();
+        response.put("applied", decision.isApplied());
+        response.put("matched", decision.isMatched());
+        response.put("code", decision.getCode());
+        response.put("requestId", decision.getRequestId());
+        response.put("tu", decision.getTu());
+        response.put("requestOrderId", decision.getRequestOrderIds());
+        response.put("orderId", decision.getOrderIds());
+        response.put("latencyMs", decision.getLatencyMs());
+        response.put("errorMessage", decision.getErrorMessage());
+        return response;
+    }
+
+    private record PlacementResolved(String platform, String appId, String tagId) {
+    }
+
+    public static class SaveOrderRequest {
+        private String tagId;
+        private String orderId;
+
+        public String getTagId() {
+            return tagId;
+        }
+
+        public void setTagId(String tagId) {
+            this.tagId = tagId;
+        }
+
+        public String getOrderId() {
+            return orderId;
+        }
+
+        public void setOrderId(String orderId) {
+            this.orderId = orderId;
+        }
+    }
+
+    public static class EvaluateRequest {
+        private String media;
+        private String tagId;
+        private String platform;
+        private Map<String, String> params;
+        private String ip;
+        private String userAgent;
+
+        public String getMedia() {
+            return media;
+        }
+
+        public void setMedia(String media) {
+            this.media = media;
+        }
+
+        public String getTagId() {
+            return tagId;
+        }
+
+        public void setTagId(String tagId) {
+            this.tagId = tagId;
+        }
+
+        public String getPlatform() {
+            return platform;
+        }
+
+        public void setPlatform(String platform) {
+            this.platform = platform;
+        }
+
+        public Map<String, String> getParams() {
+            return params;
+        }
+
+        public void setParams(Map<String, String> params) {
+            this.params = params;
+        }
+
+        public String getIp() {
+            return ip;
+        }
+
+        public void setIp(String ip) {
+            this.ip = ip;
+        }
+
+        public String getUserAgent() {
+            return userAgent;
+        }
+
+        public void setUserAgent(String userAgent) {
+            this.userAgent = userAgent;
+        }
+    }
+}

+ 66 - 40
src/main/java/com/adx/tencent/rta/BaiduRtaService.java

@@ -18,7 +18,6 @@ import java.time.Duration;
 import java.time.Instant;
 import java.util.Base64;
 import java.util.LinkedHashMap;
-import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.UUID;
@@ -34,12 +33,13 @@ public class BaiduRtaService {
     private final String sspid;
     private final String eKey;
     private final String tuPrefix;
-    private final List<String> honorOrderIds;
-    private final List<String> vivoOrderIds;
+    private final RtaOrderResolver orderResolver;
     private final HttpClient httpClient;
     private final ObjectMapper objectMapper;
 
-    public BaiduRtaService(AppProperties props, ObjectMapper objectMapper) {
+    public BaiduRtaService(AppProperties props,
+                           ObjectMapper objectMapper,
+                           RtaOrderResolver orderResolver) {
         this.enabled = props.isRtaEnabled();
         this.endpoint = props.getRtaEndpoint();
         this.timeout = props.getRtaTimeout() == null ? Duration.ofMillis(800) : props.getRtaTimeout();
@@ -50,8 +50,7 @@ public class BaiduRtaService {
         String configuredEKey = trimToNull(props.getRtaEKey());
         this.eKey = configuredEKey != null ? configuredEKey : trimToNull(props.getBaiduAuctionPriceEKey());
         this.tuPrefix = trimToNull(props.getRtaTuPrefix());
-        this.honorOrderIds = props.getHonorRtaOrderIds() == null ? List.of() : props.getHonorRtaOrderIds();
-        this.vivoOrderIds = props.getVivoRtaOrderIds() == null ? List.of() : props.getVivoRtaOrderIds();
+        this.orderResolver = orderResolver;
         this.httpClient = HttpClient.newBuilder().connectTimeout(this.timeout).build();
         this.objectMapper = objectMapper;
     }
@@ -66,15 +65,23 @@ public class BaiduRtaService {
         if (!enabled) {
             return BaiduRtaDecision.bypass();
         }
-        MediaConfig config = mediaConfig(media);
-        if (config == null || endpoint == null || endpoint.isBlank() || customerName == null || sspid == null || eKey == null) {
+        if (!supportsMedia(media) || endpoint == null || endpoint.isBlank() || customerName == null || sspid == null || eKey == null) {
             log.warn("[RTA][{}] skipped: missing global config", mediaLabel(media));
             return BaiduRtaDecision.bypass();
         }
         String tu = buildTu(tagId);
-        if (tu == null || config.orderIds.isEmpty() || appId == null || appId.isBlank()) {
-            log.warn("[RTA][{}] skipped: missing media config | tu={} orderIds={} appId={}",
-                    mediaLabel(media), tu, config.orderIds, appId);
+        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 (tu == null || appId == null || appId.isBlank()) {
+            log.warn("[RTA][{}] skipped: missing tag config | 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(
+                    "applied", false,
+                    "matched", true,
+                    "reason", "missing tag config",
+                    "latencyMs", 0
+            )));
             return BaiduRtaDecision.bypass();
         }
 
@@ -82,27 +89,12 @@ public class BaiduRtaService {
         String requestId = UUID.randomUUID().toString().replace("-", "");
         String timestamp = String.valueOf(Instant.now().getEpochSecond());
         int os = rtaOs(platform);
-        String requestOrderIds = String.join(",", config.orderIds);
-        Map<String, Object> body = new LinkedHashMap<>();
-        body.put("customerName", customerName);
-        body.put("sspid", sspid);
-        body.put("appsid", appId);
-        body.put("tu", tu);
+        Map<String, Object> body = buildRequestPreview(appId, tagId, platform, params, ip, userAgent, tu, requestOrderId, requestId, timestamp);
         body.put("sign", sign(timestamp, requestId));
-        body.put("order_id", requestOrderIds);
-        body.put("os", os);
-        putIfNotBlank(body, "oaid", androidOaid(params, platform));
-        putIfNotBlank(body, "idfa_md5", iosIdfaMd5(params, platform));
-        putIfNotBlank(body, "idfa", iosIdfa(params, platform));
-        putIfNotBlank(body, "caid_md5", iosCaidMd5(params, platform));
-        putIfNotBlank(body, "caid", iosCaid(params, platform));
-        putIfNotBlank(body, "ip", trimToNull(ip));
-        putIfNotBlank(body, "ua", trimToNull(userAgent));
-        body.put("request_id", requestId);
-        body.put("timestamp", timestamp);
 
         try {
             String requestBody = objectMapper.writeValueAsString(body);
+            log.info("[RTA][{}][request] {}", mediaLabel(media), requestBody);
             HttpRequest request = HttpRequest.newBuilder()
                     .uri(URI.create(endpoint))
                     .timeout(timeout)
@@ -111,9 +103,10 @@ 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());
             if (response.statusCode() != 200) {
                 log.warn("[RTA][{}] httpStatus={} requestId={} body={}", mediaLabel(media), response.statusCode(), requestId, response.body());
-                return BaiduRtaDecision.of(false, null, requestId, tu, requestOrderIds, null, latencyMs, "httpStatus=" + response.statusCode());
+                return BaiduRtaDecision.of(false, null, requestId, tu, requestOrderId, null, latencyMs, "httpStatus=" + response.statusCode());
             }
             JsonNode root = objectMapper.readTree(response.body());
             Integer code = root.hasNonNull("code") ? root.get("code").asInt() : null;
@@ -121,20 +114,56 @@ public class BaiduRtaService {
             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);
-            return BaiduRtaDecision.of(matched, code, requestId, tu, requestOrderIds, orderIds, latencyMs, null);
+            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, requestOrderIds, null, latencyMs, e.getMessage());
+            return BaiduRtaDecision.of(false, null, requestId, tu, requestOrderId, null, latencyMs, e.getMessage());
         }
     }
 
-    private MediaConfig mediaConfig(String media) {
-        return switch (media) {
-            case "honor" -> new MediaConfig(honorOrderIds);
-            case "vivo" -> new MediaConfig(vivoOrderIds);
-            default -> null;
-        };
+    private Map<String, Object> buildRequestPreview(String appId,
+                                                    String tagId,
+                                                    String platform,
+                                                    Map<String, String> params,
+                                                    String ip,
+                                                    String userAgent,
+                                                    String tu,
+                                                    String requestOrderId,
+                                                    String requestId,
+                                                    String timestamp) {
+        int os = rtaOs(platform);
+        Map<String, Object> body = new LinkedHashMap<>();
+        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);
+        putIfNotBlank(body, "oaid", androidOaid(params, platform));
+        putIfNotBlank(body, "idfa_md5", iosIdfaMd5(params, platform));
+        putIfNotBlank(body, "idfa", iosIdfa(params, platform));
+        putIfNotBlank(body, "caid_md5", iosCaidMd5(params, platform));
+        putIfNotBlank(body, "caid", iosCaid(params, platform));
+        putIfNotBlank(body, "ip", trimToNull(ip));
+        putIfNotBlank(body, "ua", trimToNull(userAgent));
+        putIfNotBlank(body, "request_id", requestId);
+        putIfNotBlank(body, "timestamp", timestamp);
+        return body;
+    }
+
+    private String toJson(Object value) {
+        try {
+            return objectMapper.writeValueAsString(value);
+        } catch (Exception e) {
+            return String.valueOf(value);
+        }
+    }
+
+    private static boolean supportsMedia(String media) {
+        return "honor".equals(media) || "vivo".equals(media);
     }
 
     private String buildTu(String tagId) {
@@ -233,7 +262,4 @@ public class BaiduRtaService {
         String trimmed = value.trim();
         return trimmed.isEmpty() ? null : trimmed;
     }
-
-    private record MediaConfig(List<String> orderIds) {
-    }
 }

+ 92 - 0
src/main/java/com/adx/tencent/rta/RtaOrderResolver.java

@@ -0,0 +1,92 @@
+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;
+import org.springframework.lang.Nullable;
+import org.springframework.stereotype.Component;
+
+import java.time.Duration;
+
+@Component
+public class RtaOrderResolver {
+    private static final Logger log = LoggerFactory.getLogger(RtaOrderResolver.class);
+
+    private final StringRedisTemplate redis;
+    private final RtaOrderStore store;
+    private final String keyPrefix;
+    private final Duration ttl;
+
+    public RtaOrderResolver(StringRedisTemplate redis,
+                            @Nullable RtaOrderStore store,
+                            AppProperties props) {
+        this.redis = redis;
+        this.store = store;
+        this.keyPrefix = props.getRtaOrderRedisPrefix() == null ? "adx:rta:order:" : props.getRtaOrderRedisPrefix();
+        this.ttl = props.getRtaOrderCacheTtl() == null ? Duration.ofHours(24) : props.getRtaOrderCacheTtl();
+    }
+
+    public String resolveOrderId(String tagId) {
+        String normalizedTagId = normalize(tagId);
+        if (normalizedTagId == null) return null;
+
+        try {
+            String cached = normalize(redis.opsForValue().get(redisKey(normalizedTagId)));
+            if (cached != null) {
+                return cached;
+            }
+        } catch (Exception e) {
+            log.warn("[RTA][Order] read redis failed | tagId={} | error={}", normalizedTagId, e.getMessage());
+        }
+
+        if (store == null) {
+            return null;
+        }
+
+        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);
+        String normalizedOrderId = normalize(orderId);
+        if (normalizedTagId == null || normalizedOrderId == null || store == null) {
+            return;
+        }
+        store.save(normalizedTagId, normalizedOrderId);
+        cache(normalizedTagId, normalizedOrderId);
+    }
+
+    private void cache(String tagId, String orderId) {
+        try {
+            if (ttl != null && !ttl.isZero() && !ttl.isNegative()) {
+                redis.opsForValue().set(redisKey(tagId), orderId, ttl);
+            } else {
+                redis.opsForValue().set(redisKey(tagId), orderId);
+            }
+        } catch (Exception e) {
+            log.warn("[RTA][Order] write redis failed | tagId={} | error={}", tagId, e.getMessage());
+        }
+    }
+
+    private String redisKey(String tagId) {
+        return keyPrefix + tagId;
+    }
+
+    private static String normalize(String value) {
+        if (value == null) return null;
+        String trimmed = value.trim();
+        return trimmed.isEmpty() ? null : trimmed;
+    }
+}

+ 53 - 0
src/main/java/com/adx/tencent/rta/RtaOrderStore.java

@@ -0,0 +1,53 @@
+package com.adx.tencent.rta;
+
+import com.adx.tencent.rta.mapper.RtaTagOrderMapper;
+import com.adx.tencent.rta.model.RtaTagOrderRecord;
+import org.apache.ibatis.session.SqlSession;
+import org.apache.ibatis.session.SqlSessionFactory;
+
+import java.sql.Statement;
+import java.time.Instant;
+
+public class RtaOrderStore {
+    private final SqlSessionFactory sqlSessionFactory;
+
+    public RtaOrderStore(SqlSessionFactory sqlSessionFactory) {
+        this.sqlSessionFactory = sqlSessionFactory;
+    }
+
+    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.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;
+        try (SqlSession session = sqlSessionFactory.openSession(true)) {
+            return session.getMapper(RtaTagOrderMapper.class).selectByTagId(tagId.trim());
+        }
+    }
+
+    public void save(String tagId, String orderId) {
+        if (tagId == null || tagId.isBlank() || orderId == null || orderId.isBlank()) return;
+        RtaTagOrderRecord record = new RtaTagOrderRecord();
+        record.setTagId(tagId.trim());
+        record.setOrderId(orderId.trim());
+        record.setUpdatedAt(Instant.now());
+        try (SqlSession session = sqlSessionFactory.openSession(true)) {
+            session.getMapper(RtaTagOrderMapper.class).insertOrUpdate(record);
+        }
+    }
+}

+ 12 - 0
src/main/java/com/adx/tencent/rta/mapper/RtaTagOrderMapper.java

@@ -0,0 +1,12 @@
+package com.adx.tencent.rta.mapper;
+
+import com.adx.tencent.rta.model.RtaTagOrderRecord;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+@Mapper
+public interface RtaTagOrderMapper {
+    RtaTagOrderRecord selectByTagId(@Param("tagId") String tagId);
+
+    void insertOrUpdate(@Param("record") RtaTagOrderRecord record);
+}

+ 22 - 0
src/main/java/com/adx/tencent/rta/model/RtaTagOrderRecord.java

@@ -0,0 +1,22 @@
+package com.adx.tencent.rta.model;
+
+import java.time.Instant;
+
+public class RtaTagOrderRecord {
+    private String tagId;
+    private String orderId;
+    private Instant updatedAt;
+
+    public String getTagId() { return tagId; }
+    public void setTagId(String tagId) { this.tagId = tagId; }
+
+    public String getOrderId() { return orderId; }
+    public void setOrderId(String orderId) { this.orderId = orderId; }
+
+    public Instant getUpdatedAt() { return updatedAt; }
+    public void setUpdatedAt(Instant updatedAt) { this.updatedAt = updatedAt; }
+
+    public java.sql.Timestamp getUpdatedAtTs() {
+        return updatedAt == null ? null : java.sql.Timestamp.from(updatedAt);
+    }
+}

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

@@ -43,9 +43,9 @@ adx:
     "9": CUSTOM            # 自定义-3日留存
 
   # --- Redis ---
-  redis-addr: "cq-crs-47p6pmwo.sql.tencentcdb.com:28036"
-  redis-username: "hcst"
-  redis-password: "hcst@2026qwe.."
+  redis-addr: "127.0.0.1:6379"
+  redis-username:
+  redis-password:
   redis-db: 5
   redis-connect-timeout: 5s
   redis-command-timeout: 5s

+ 28 - 0
src/main/resources/mapper/RtaTagOrderMapper.xml

@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.adx.tencent.rta.mapper.RtaTagOrderMapper">
+    <resultMap id="rtaTagOrderResultMap" type="com.adx.tencent.rta.model.RtaTagOrderRecord">
+        <result property="tagId" column="tag_id"/>
+        <result property="orderId" column="order_id"/>
+        <result property="updatedAt" column="updated_at"/>
+    </resultMap>
+
+    <select id="selectByTagId" resultMap="rtaTagOrderResultMap">
+        SELECT tag_id, order_id, updated_at
+        FROM rta_tag_orders
+        WHERE tag_id = #{tagId}
+        LIMIT 1
+    </select>
+
+    <insert id="insertOrUpdate">
+        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>