yumeng 1 bulan lalu
induk
melakukan
d941f67cb8

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

@@ -39,6 +39,15 @@ public class AppProperties {
     private int baiduActionType = 1;
     private int baiduTrackingMaxRedirects = 20;
     private int baiduTrackingMax500Retries = 3;
+    private boolean rtaEnabled = false;
+    private String rtaEndpoint = "https://baidu.rta-gateway.com:8000/rta-proxy";
+    private Duration rtaTimeout = Duration.ofMillis(800);
+    private String rtaCustomerName;
+    private String rtaSspid;
+    private String rtaEKey;
+    private String rtaTuPrefix = "huichuang1213";
+    private List<String> honorRtaOrderIds = new ArrayList<>();
+    private List<String> vivoRtaOrderIds = new ArrayList<>();
 
     // Honor Callback
     private String honorConversionBaseUrl = "https://ads-drcn.platform.hihonorcloud.com";
@@ -320,6 +329,78 @@ public class AppProperties {
         this.baiduTrackingMax500Retries = v;
     }
 
+    public boolean isRtaEnabled() {
+        return rtaEnabled;
+    }
+
+    public void setRtaEnabled(boolean rtaEnabled) {
+        this.rtaEnabled = rtaEnabled;
+    }
+
+    public String getRtaEndpoint() {
+        return rtaEndpoint;
+    }
+
+    public void setRtaEndpoint(String rtaEndpoint) {
+        this.rtaEndpoint = rtaEndpoint;
+    }
+
+    public Duration getRtaTimeout() {
+        return rtaTimeout;
+    }
+
+    public void setRtaTimeout(Duration rtaTimeout) {
+        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;
+    }
+
+    public List<String> getHonorRtaOrderIds() {
+        return honorRtaOrderIds;
+    }
+
+    public void setHonorRtaOrderIds(List<String> honorRtaOrderIds) {
+        this.honorRtaOrderIds = honorRtaOrderIds;
+    }
+
+    public void setRtaTuPrefix(String rtaTuPrefix) {
+        this.rtaTuPrefix = rtaTuPrefix;
+    }
+
+    public List<String> getVivoRtaOrderIds() {
+        return vivoRtaOrderIds;
+    }
+
+    public void setVivoRtaOrderIds(List<String> vivoRtaOrderIds) {
+        this.vivoRtaOrderIds = vivoRtaOrderIds;
+    }
+
     public String getHonorConversionBaseUrl() {
         return honorConversionBaseUrl;
     }

+ 124 - 2
src/main/java/com/adx/tencent/honor/controller/HonorTrackingController.java

@@ -5,7 +5,10 @@ import com.adx.tencent.baidu.model.NormalizedBidResponse;
 import com.adx.tencent.baidu.model.TrackingResult;
 import com.adx.tencent.honor.model.HonorBidRecord;
 import com.adx.tencent.honor.model.HonorTrackingRecord;
+import com.adx.tencent.honor.store.HonorColdStore;
 import com.adx.tencent.honor.service.HonorPlacement;
+import com.adx.tencent.rta.BaiduRtaDecision;
+import com.adx.tencent.rta.BaiduRtaService;
 import com.adx.tencent.honor.store.HonorHotStore;
 import com.adx.tencent.util.TraceIds;
 import com.fasterxml.jackson.databind.ObjectMapper;
@@ -41,16 +44,22 @@ public class HonorTrackingController {
 
     private final AdxClient adxClient;
     private final HonorHotStore hotStore;
+    private final HonorColdStore coldStore;
     private final HonorPlacement placement;
+    private final BaiduRtaService rtaService;
     private final ObjectMapper objectMapper;
 
     public HonorTrackingController(AdxClient adxClient,
                                    HonorHotStore hotStore,
+                                   @Nullable HonorColdStore coldStore,
                                    @Nullable HonorPlacement placement,
+                                   @Nullable BaiduRtaService rtaService,
                                    ObjectMapper objectMapper) {
         this.adxClient = adxClient;
         this.hotStore = hotStore;
+        this.coldStore = coldStore;
         this.placement = placement;
+        this.rtaService = rtaService;
         this.objectMapper = objectMapper;
     }
 
@@ -85,6 +94,15 @@ public class HonorTrackingController {
         }
         log.info("[荣耀][曝光][入参] traceId={} ip={} params={}", traceId, clientIp(request), params);
 
+        HonorPlacement.Resolved resolved = placement.resolve(params);
+        BaiduRtaDecision rtaDecision = evaluateRta(traceId, resolved, params, request);
+        Map<String, String> rtaParams = enrichMediaParams(params, rtaDecision);
+        if (!rtaDecision.allowsBid()) {
+            HonorBidRecord blocked = blockedBidRecord(traceId, "impression", rtaParams, resolved, rtaDecision);
+            saveBlockedBid(blocked);
+            return Map.of("qk", blocked.getQk(), "traceId", traceId, "tagId", tagId, "rtaCode", safeInt(blocked.getRtaCode()));
+        }
+
         NormalizedBidResponse response = adxClient.requestBid("honor", traceId, buildBidRequest(request, traceId, params));
 
         if (response == null) {
@@ -100,7 +118,8 @@ public class HonorTrackingController {
                 response.getBid() != null && response.getBid().getClickUrls() != null ? response.getBid().getClickUrls().size() : 0,
                 response.getBid() != null && response.getBid().getShowUrls() != null ? response.getBid().getShowUrls().size() : 0);
 
-        HonorBidRecord record = bidRecordFromResponse(response, traceId, "impression", params);
+        HonorBidRecord record = bidRecordFromResponse(response, traceId, "impression", rtaParams);
+        record.setRtaCode(rtaDecision.getCode());
         hotStore.recordBid(record);
 
         List<TrackingResult> tracking = Collections.emptyList();
@@ -124,6 +143,14 @@ public class HonorTrackingController {
 
         HonorBidRecord record = hotStore.findBidByMediaTrace(traceId);
         if (record == null) {
+            HonorPlacement.Resolved resolved = placement.resolve(params);
+            BaiduRtaDecision 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()));
+            }
             NormalizedBidResponse bidResponse = adxClient.requestBid("honor", traceId, buildBidRequest(request, traceId, params));
             if (bidResponse == null) {
                 log.warn("[荣耀][点击][百度结果] traceId={} empty=true", traceId);
@@ -137,7 +164,8 @@ public class HonorTrackingController {
                     bidResponse.getBid() != null ? bidResponse.getBid().getPrice() : 0,
                     bidResponse.getBid() != null && bidResponse.getBid().getClickUrls() != null ? bidResponse.getBid().getClickUrls().size() : 0,
                     bidResponse.getBid() != null && bidResponse.getBid().getShowUrls() != null ? bidResponse.getBid().getShowUrls().size() : 0);
-            record = bidRecordFromResponse(bidResponse, traceId, "click", params);
+            record = bidRecordFromResponse(bidResponse, traceId, "click", rtaParams);
+            record.setRtaCode(rtaDecision.getCode());
             hotStore.recordBid(record);
             if (bidResponse.getBid() != null && bidResponse.getBid().getShowUrls() != null && !bidResponse.getBid().getShowUrls().isEmpty()) {
                 List<TrackingResult> impressionTracking = adxClient.reportImpression(
@@ -266,6 +294,91 @@ public class HonorTrackingController {
         return record;
     }
 
+    private BaiduRtaDecision evaluateRta(String traceId,
+                                         HonorPlacement.Resolved resolved,
+                                         Map<String, String> params,
+                                         HttpServletRequest request) {
+        if (rtaService == null) {
+            return BaiduRtaDecision.bypass();
+        }
+        BaiduRtaDecision decision = rtaService.evaluate(
+                "honor",
+                resolved.appId(),
+                resolved.tagId(),
+                resolved.platform(),
+                params,
+                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;
+    }
+
+    private HonorBidRecord blockedBidRecord(String traceId,
+                                            String eventType,
+                                            Map<String, String> params,
+                                            HonorPlacement.Resolved resolved,
+                                            BaiduRtaDecision decision) {
+        HonorBidRecord record = new HonorBidRecord();
+        record.setQk(blockedQk("honor", decision.getRequestId(), traceId));
+        record.setMedia("honor");
+        record.setMediaTraceId(traceId);
+        record.setPlatform(resolved.platform());
+        record.setAccountId(params.get("advertiserId"));
+        record.setTagId(resolved.tagId());
+        record.setEventType(eventType);
+        record.setPackageName(firstParam(params, "pkgName", "pkg_name", "packageName"));
+        record.setMediaParams(enrichMediaParams(params, decision));
+        record.setRtaCode(decision.getCode());
+        record.setPrice(0L);
+        record.setCreatedAt(Instant.now());
+        return record;
+    }
+
+    private void saveBlockedBid(HonorBidRecord record) {
+        if (coldStore == null) {
+            throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "honor cold store not configured");
+        }
+        coldStore.saveBid(record);
+    }
+
+    private static Map<String, String> enrichMediaParams(Map<String, String> params, BaiduRtaDecision decision) {
+        Map<String, String> enriched = new LinkedHashMap<>();
+        if (params != null) {
+            enriched.putAll(params);
+        }
+        if (decision == null || !decision.isApplied()) {
+            return enriched;
+        }
+        putIfNotBlank(enriched, "rta_tu", decision.getTu());
+        putIfNotBlank(enriched, "rta_order_id", firstNonBlank(decision.getOrderIds(), decision.getRequestOrderIds()));
+        putIfNotBlank(enriched, "rta_request_order_id", decision.getRequestOrderIds());
+        putIfNotBlank(enriched, "rta_request_id", decision.getRequestId());
+        if (decision.getCode() != null) {
+            enriched.put("rta_code", String.valueOf(decision.getCode()));
+        }
+        return enriched;
+    }
+
+    private static void putIfNotBlank(Map<String, String> params, String key, String value) {
+        if (value != null && !value.isBlank()) {
+            params.put(key, value);
+        }
+    }
+
+    private static String firstNonBlank(String... values) {
+        if (values == null) return null;
+        for (String value : values) {
+            if (value != null && !value.isBlank()) {
+                return value;
+            }
+        }
+        return null;
+    }
+
     private void recordTracking(String qk, String tagId, String kind, List<TrackingResult> results) {
         if (results == null) return;
         for (TrackingResult result : results) {
@@ -446,4 +559,13 @@ public class HonorTrackingController {
             return String.valueOf(value);
         }
     }
+
+    private static int safeInt(Integer value) {
+        return value == null ? 0 : value;
+    }
+
+    private static String blockedQk(String media, String requestId, String traceId) {
+        String suffix = requestId != null && !requestId.isBlank() ? requestId : md5Hex(traceId == null ? media : traceId + "-" + Instant.now().toEpochMilli());
+        return "rta-" + media + "-" + suffix;
+    }
 }

+ 12 - 1
src/main/java/com/adx/tencent/honor/model/HonorBidRecord.java

@@ -20,6 +20,7 @@ public class HonorBidRecord {
     private String creativeId;
     private String impId;
     private String tagId;
+    private Integer rtaCode;
     private Long price;
     private List<String> showUrls;
     private List<String> clickUrls;
@@ -53,6 +54,8 @@ public class HonorBidRecord {
     public void setImpId(String v) { this.impId = v; }
     public String getTagId() { return tagId; }
     public void setTagId(String v) { this.tagId = v; }
+    public Integer getRtaCode() { return rtaCode; }
+    public void setRtaCode(Integer v) { this.rtaCode = v; }
     public Long getPrice() { return price; }
     public void setPrice(Long v) { this.price = v == null ? 0L : v; }
     public List<String> getShowUrls() { return showUrls; }
@@ -78,6 +81,7 @@ public class HonorBidRecord {
         slim.mediaTraceId = this.mediaTraceId;
         slim.platform = this.platform;
         slim.tagId = this.tagId;
+        slim.rtaCode = this.rtaCode;
         slim.mediaParams = this.mediaParams;
         slim.packageName = this.packageName;
         return slim;
@@ -90,6 +94,7 @@ public class HonorBidRecord {
         hot.mediaTraceId = this.mediaTraceId;
         hot.platform = this.platform;
         hot.tagId = this.tagId;
+        hot.rtaCode = this.rtaCode;
         hot.adId = this.adId;
         hot.accountId = this.accountId;
         hot.clickUrls = this.clickUrls;
@@ -109,6 +114,7 @@ public class HonorBidRecord {
         cold.accountId = this.accountId;
         cold.eventType = this.eventType;
         cold.tagId = this.tagId;
+        cold.rtaCode = this.rtaCode;
         cold.price = this.price == null ? 0L : this.price;
         cold.packageName = this.packageName;
         cold.mediaParams = slimDeviceParams(this.mediaParams);
@@ -123,7 +129,12 @@ public class HonorBidRecord {
                 "pkgName", "channelId", "tagId", "ua", "ip",
                 "oaid", "oaidMd5", "oaid_md5", "hash_oaid",
                 "dpid", "dpidMd5", "dpid_md5", "hash_android_id",
-                "muid"
+                "muid",
+                "rta_tu",
+                "rta_order_id",
+                "rta_request_order_id",
+                "rta_request_id",
+                "rta_code"
         );
         Map<String, String> slim = new LinkedHashMap<>();
         for (String key : keepKeys) {

+ 5 - 0
src/main/java/com/adx/tencent/honor/store/HonorColdStore.java

@@ -41,6 +41,7 @@ public class HonorColdStore {
                     ad_id VARCHAR(128),
                     account_id VARCHAR(128),
                     event_type VARCHAR(32),
+                    rta_code INT,
                     req_id VARCHAR(512),
                     bid_id VARCHAR(128),
                     creative_id VARCHAR(128),
@@ -113,6 +114,10 @@ public class HonorColdStore {
             stmt.execute("UPDATE honor_ad_bid_events SET price = 0 WHERE price IS NULL");
             stmt.execute("ALTER TABLE honor_ad_bid_events MODIFY COLUMN price BIGINT UNSIGNED NOT NULL DEFAULT 0");
             try {
+                stmt.execute("ALTER TABLE honor_ad_bid_events ADD COLUMN rta_code INT COMMENT 'RTA 状态码' AFTER event_type");
+            } catch (Exception ignored) {
+            }
+            try {
                 stmt.execute("ALTER TABLE honor_tracking_reports ADD KEY idx_honor_tracking_reports_created_at_id (created_at, id)");
             } catch (Exception ignored) {
             }

+ 59 - 0
src/main/java/com/adx/tencent/rta/BaiduRtaDecision.java

@@ -0,0 +1,59 @@
+package com.adx.tencent.rta;
+
+public class BaiduRtaDecision {
+    private final boolean applied;
+    private final boolean matched;
+    private final Integer code;
+    private final String requestId;
+    private final String tu;
+    private final String requestOrderIds;
+    private final String orderIds;
+    private final long latencyMs;
+    private final String errorMessage;
+
+    private BaiduRtaDecision(boolean applied,
+                             boolean matched,
+                             Integer code,
+                             String requestId,
+                             String tu,
+                             String requestOrderIds,
+                             String orderIds,
+                             long latencyMs,
+                             String errorMessage) {
+        this.applied = applied;
+        this.matched = matched;
+        this.code = code;
+        this.requestId = requestId;
+        this.tu = tu;
+        this.requestOrderIds = requestOrderIds;
+        this.orderIds = orderIds;
+        this.latencyMs = latencyMs;
+        this.errorMessage = errorMessage;
+    }
+
+    public static BaiduRtaDecision bypass() {
+        return new BaiduRtaDecision(false, true, null, null, null, null, null, 0L, null);
+    }
+
+    public static BaiduRtaDecision of(boolean matched,
+                                      Integer code,
+                                      String requestId,
+                                      String tu,
+                                      String requestOrderIds,
+                                      String orderIds,
+                                      long latencyMs,
+                                      String errorMessage) {
+        return new BaiduRtaDecision(true, matched, code, requestId, tu, requestOrderIds, orderIds, latencyMs, errorMessage);
+    }
+
+    public boolean isApplied() { return applied; }
+    public boolean isMatched() { return matched; }
+    public Integer getCode() { return code; }
+    public String getRequestId() { return requestId; }
+    public String getTu() { return tu; }
+    public String getRequestOrderIds() { return requestOrderIds; }
+    public String getOrderIds() { return orderIds; }
+    public long getLatencyMs() { return latencyMs; }
+    public String getErrorMessage() { return errorMessage; }
+    public boolean allowsBid() { return !applied || matched; }
+}

+ 239 - 0
src/main/java/com/adx/tencent/rta/BaiduRtaService.java

@@ -0,0 +1,239 @@
+package com.adx.tencent.rta;
+
+import com.adx.tencent.config.AppProperties;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.SecretKeySpec;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Base64;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.UUID;
+
+@Component
+public class BaiduRtaService {
+    private static final Logger log = LoggerFactory.getLogger(BaiduRtaService.class);
+
+    private final boolean enabled;
+    private final String endpoint;
+    private final Duration timeout;
+    private final String customerName;
+    private final String sspid;
+    private final String eKey;
+    private final String tuPrefix;
+    private final List<String> honorOrderIds;
+    private final List<String> vivoOrderIds;
+    private final HttpClient httpClient;
+    private final ObjectMapper objectMapper;
+
+    public BaiduRtaService(AppProperties props, ObjectMapper objectMapper) {
+        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.tuPrefix = trimToNull(props.getRtaTuPrefix());
+        this.honorOrderIds = props.getHonorRtaOrderIds() == null ? List.of() : props.getHonorRtaOrderIds();
+        this.vivoOrderIds = props.getVivoRtaOrderIds() == null ? List.of() : props.getVivoRtaOrderIds();
+        this.httpClient = HttpClient.newBuilder().connectTimeout(this.timeout).build();
+        this.objectMapper = objectMapper;
+    }
+
+    public BaiduRtaDecision evaluate(String media,
+                                     String appId,
+                                     String tagId,
+                                     String platform,
+                                     Map<String, String> params,
+                                     String ip,
+                                     String userAgent) {
+        if (!enabled) {
+            return BaiduRtaDecision.bypass();
+        }
+        MediaConfig config = mediaConfig(media);
+        if (config == null || 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);
+            return BaiduRtaDecision.bypass();
+        }
+
+        long startedAt = System.nanoTime();
+        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);
+        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);
+            HttpRequest request = HttpRequest.newBuilder()
+                    .uri(URI.create(endpoint))
+                    .timeout(timeout)
+                    .header("Content-Type", "application/json")
+                    .POST(HttpRequest.BodyPublishers.ofString(requestBody, StandardCharsets.UTF_8))
+                    .build();
+            HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
+            long latencyMs = (System.nanoTime() - startedAt) / 1_000_000L;
+            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());
+            }
+            JsonNode root = objectMapper.readTree(response.body());
+            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);
+            return BaiduRtaDecision.of(matched, code, requestId, tu, requestOrderIds, 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());
+        }
+    }
+
+    private MediaConfig mediaConfig(String media) {
+        return switch (media) {
+            case "honor" -> new MediaConfig(honorOrderIds);
+            case "vivo" -> new MediaConfig(vivoOrderIds);
+            default -> null;
+        };
+    }
+
+    private String buildTu(String tagId) {
+        String normalizedTagId = trimToNull(tagId);
+        if (tuPrefix == null || normalizedTagId == null) {
+            return null;
+        }
+        return tuPrefix + normalizedTagId;
+    }
+
+    private String sign(String timestamp, String requestId) {
+        try {
+            byte[] rawKey = eKey.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");
+            cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"));
+            byte[] encrypted = cipher.doFinal((timestamp + "\n" + requestId).getBytes(StandardCharsets.UTF_8));
+            return Base64.getEncoder().encodeToString(encrypted);
+        } catch (Exception e) {
+            throw new RuntimeException("build RTA sign failed", e);
+        }
+    }
+
+    private static int rtaOs(String platform) {
+        String normalized = trimToNull(platform);
+        if (normalized == null) return 0;
+        return switch (normalized.toLowerCase(Locale.ROOT)) {
+            case "ios" -> 1;
+            case "android" -> 2;
+            case "harmony", "hongmeng" -> 5;
+            default -> 0;
+        };
+    }
+
+    private static String androidOaid(Map<String, String> params, String platform) {
+        int os = rtaOs(platform);
+        if (os != 2 && os != 5) return null;
+        return firstParam(params, "oaid");
+    }
+
+    private static String iosIdfaMd5(Map<String, String> params, String platform) {
+        if (rtaOs(platform) != 1) return null;
+        return firstParam(params, "idfaMD5", "idfa_md5", "idfaMd5");
+    }
+
+    private static String iosIdfa(Map<String, String> params, String platform) {
+        if (rtaOs(platform) != 1) return null;
+        return firstParam(params, "idfa");
+    }
+
+    private static String iosCaidMd5(Map<String, String> params, String platform) {
+        if (rtaOs(platform) != 1) return null;
+        return firstParam(params, "caid_md5", "caidMd5");
+    }
+
+    private static String iosCaid(Map<String, String> params, String platform) {
+        if (rtaOs(platform) != 1) return null;
+        String raw = firstParam(params, "caid", "kenyid_caa", "kenyIdCaa");
+        if (raw == null) return null;
+        int idx = raw.indexOf(',');
+        return idx >= 0 ? trimToNull(raw.substring(0, idx)) : trimToNull(raw);
+    }
+
+    private static String firstParam(Map<String, String> params, String... keys) {
+        if (params == null) return null;
+        for (String key : keys) {
+            String value = trimToNull(params.get(key));
+            if (value != null) {
+                return value;
+            }
+        }
+        return null;
+    }
+
+    private static void putIfNotBlank(Map<String, Object> body, String key, String value) {
+        String normalized = trimToNull(value);
+        if (normalized != null) {
+            body.put(key, normalized);
+        }
+    }
+
+    private static String mediaLabel(String media) {
+        return switch (media) {
+            case "honor" -> "荣耀";
+            case "vivo" -> "vivo";
+            default -> media == null ? "未知" : media;
+        };
+    }
+
+    private static String trimToNull(String value) {
+        if (value == null) return null;
+        String trimmed = value.trim();
+        return trimmed.isEmpty() ? null : trimmed;
+    }
+
+    private record MediaConfig(List<String> orderIds) {
+    }
+}

+ 124 - 2
src/main/java/com/adx/tencent/vivo/controller/VivoTrackingController.java

@@ -3,11 +3,14 @@ package com.adx.tencent.vivo.controller;
 import com.adx.tencent.baidu.AdxClient;
 import com.adx.tencent.baidu.model.NormalizedBidResponse;
 import com.adx.tencent.baidu.model.TrackingResult;
+import com.adx.tencent.rta.BaiduRtaDecision;
+import com.adx.tencent.rta.BaiduRtaService;
 import com.adx.tencent.vivo.model.VivoBidRecord;
 import com.adx.tencent.vivo.model.VivoTrackingRecord;
 import com.adx.tencent.vivo.service.VivoAdvertiserSyncService;
 import com.adx.tencent.vivo.service.VivoAuthService;
 import com.adx.tencent.vivo.service.VivoPlacement;
+import com.adx.tencent.vivo.store.VivoColdStore;
 import com.adx.tencent.vivo.store.VivoHotStore;
 import com.adx.tencent.util.TraceIds;
 import com.fasterxml.jackson.databind.ObjectMapper;
@@ -43,22 +46,28 @@ public class VivoTrackingController {
 
     private final AdxClient adxClient;
     private final VivoHotStore hotStore;
+    private final VivoColdStore coldStore;
     private final VivoPlacement placement;
     private final VivoAuthService authService;
     private final VivoAdvertiserSyncService advertiserSyncService;
+    private final BaiduRtaService rtaService;
     private final ObjectMapper objectMapper;
 
     public VivoTrackingController(AdxClient adxClient,
                                   @Nullable VivoHotStore hotStore,
+                                  @Nullable VivoColdStore coldStore,
                                   @Nullable VivoPlacement placement,
                                   @Nullable VivoAuthService authService,
                                   @Nullable VivoAdvertiserSyncService advertiserSyncService,
+                                  @Nullable BaiduRtaService rtaService,
                                   ObjectMapper objectMapper) {
         this.adxClient = adxClient;
         this.hotStore = hotStore;
+        this.coldStore = coldStore;
         this.placement = placement;
         this.authService = authService;
         this.advertiserSyncService = advertiserSyncService;
+        this.rtaService = rtaService;
         this.objectMapper = objectMapper;
     }
 
@@ -137,6 +146,15 @@ public class VivoTrackingController {
         requireVivoParams(params);
         log.info("[Vivo][曝光][入参] traceId={} ip={} params={}", traceId, clientIp(request), params);
 
+        VivoPlacement.Resolved resolved = placement.resolve(params);
+        BaiduRtaDecision rtaDecision = evaluateRta(traceId, resolved, params, request);
+        Map<String, String> rtaParams = enrichMediaParams(params, rtaDecision);
+        if (!rtaDecision.allowsBid()) {
+            VivoBidRecord blocked = blockedBidRecord(traceId, "impression", rtaParams, resolved, rtaDecision);
+            saveBlockedBid(blocked);
+            return Map.of("qk", blocked.getQk(), "traceId", traceId, "tagId", blocked.getTagId(), "rtaCode", safeInt(blocked.getRtaCode()));
+        }
+
         Map<String, Object> bidRequest = buildBidRequest(request, traceId, params);
         NormalizedBidResponse response = adxClient.requestBid("vivo", traceId, bidRequest);
         if (response == null) {
@@ -152,7 +170,8 @@ public class VivoTrackingController {
                 response.getBid() != null && response.getBid().getClickUrls() != null ? response.getBid().getClickUrls().size() : 0,
                 response.getBid() != null && response.getBid().getShowUrls() != null ? response.getBid().getShowUrls().size() : 0);
 
-        VivoBidRecord record = bidRecordFromResponse(response, traceId, "impression", params);
+        VivoBidRecord record = bidRecordFromResponse(response, traceId, "impression", rtaParams);
+        record.setRtaCode(rtaDecision.getCode());
         hotStore.recordBid(record);
 
         if (response.getBid() != null && response.getBid().getShowUrls() != null && !response.getBid().getShowUrls().isEmpty()) {
@@ -175,6 +194,14 @@ public class VivoTrackingController {
 
         VivoBidRecord record = hotStore.findBidByMediaTrace(traceId);
         if (record == null) {
+            VivoPlacement.Resolved resolved = placement.resolve(params);
+            BaiduRtaDecision 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()));
+            }
             NormalizedBidResponse bidResponse = adxClient.requestBid("vivo", traceId, buildBidRequest(request, traceId, params));
             if (bidResponse == null) {
                 log.warn("[Vivo][点击][百度结果] traceId={} empty=true", traceId);
@@ -189,7 +216,8 @@ public class VivoTrackingController {
                     bidResponse.getBid() != null && bidResponse.getBid().getClickUrls() != null ? bidResponse.getBid().getClickUrls().size() : 0,
                     bidResponse.getBid() != null && bidResponse.getBid().getShowUrls() != null ? bidResponse.getBid().getShowUrls().size() : 0);
 
-            record = bidRecordFromResponse(bidResponse, traceId, "click", params);
+            record = bidRecordFromResponse(bidResponse, traceId, "click", rtaParams);
+            record.setRtaCode(rtaDecision.getCode());
             hotStore.recordBid(record);
             if (bidResponse.getBid() != null && bidResponse.getBid().getShowUrls() != null && !bidResponse.getBid().getShowUrls().isEmpty()) {
                 List<TrackingResult> impressionTracking = adxClient.reportImpression(
@@ -341,6 +369,91 @@ public class VivoTrackingController {
         return record;
     }
 
+    private BaiduRtaDecision evaluateRta(String traceId,
+                                         VivoPlacement.Resolved resolved,
+                                         Map<String, String> params,
+                                         HttpServletRequest request) {
+        if (rtaService == null) {
+            return BaiduRtaDecision.bypass();
+        }
+        BaiduRtaDecision decision = rtaService.evaluate(
+                "vivo",
+                resolved.appId(),
+                resolved.tagId(),
+                resolved.platform(),
+                params,
+                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;
+    }
+
+    private VivoBidRecord blockedBidRecord(String traceId,
+                                           String eventType,
+                                           Map<String, String> params,
+                                           VivoPlacement.Resolved resolved,
+                                           BaiduRtaDecision decision) {
+        VivoBidRecord record = new VivoBidRecord();
+        record.setQk(blockedQk("vivo", decision.getRequestId(), traceId));
+        record.setMedia("vivo");
+        record.setMediaTraceId(traceId);
+        record.setPlatform(resolved.platform());
+        record.setAccountId(firstParam(params, "accountId", "accountid"));
+        record.setEventType(eventType);
+        record.setTagId(resolved.tagId());
+        record.setPackageName(firstParam(params, "pkgName", "packageName"));
+        record.setMediaParams(enrichMediaParams(params, decision));
+        record.setRtaCode(decision.getCode());
+        record.setPrice(0L);
+        record.setCreatedAt(Instant.now());
+        return record;
+    }
+
+    private void saveBlockedBid(VivoBidRecord record) {
+        if (coldStore == null) {
+            throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "vivo cold store not configured");
+        }
+        coldStore.saveBid(record);
+    }
+
+    private static Map<String, String> enrichMediaParams(Map<String, String> params, BaiduRtaDecision decision) {
+        Map<String, String> enriched = new LinkedHashMap<>();
+        if (params != null) {
+            enriched.putAll(params);
+        }
+        if (decision == null || !decision.isApplied()) {
+            return enriched;
+        }
+        putIfNotBlank(enriched, "rta_tu", decision.getTu());
+        putIfNotBlank(enriched, "rta_order_id", firstNonBlank(decision.getOrderIds(), decision.getRequestOrderIds()));
+        putIfNotBlank(enriched, "rta_request_order_id", decision.getRequestOrderIds());
+        putIfNotBlank(enriched, "rta_request_id", decision.getRequestId());
+        if (decision.getCode() != null) {
+            enriched.put("rta_code", String.valueOf(decision.getCode()));
+        }
+        return enriched;
+    }
+
+    private static void putIfNotBlank(Map<String, String> params, String key, String value) {
+        if (value != null && !value.isBlank()) {
+            params.put(key, value);
+        }
+    }
+
+    private static String firstNonBlank(String... values) {
+        if (values == null) return null;
+        for (String value : values) {
+            if (value != null && !value.isBlank()) {
+                return value;
+            }
+        }
+        return null;
+    }
+
     private Map<String, Object> baiduUidFromParams(Map<String, String> p, String platform) {
         Map<String, Object> uid = new LinkedHashMap<>();
         if ("android".equals(platform)) {
@@ -674,6 +787,15 @@ public class VivoTrackingController {
         return value == null ? "" : value;
     }
 
+    private static int safeInt(Integer value) {
+        return value == null ? 0 : value;
+    }
+
+    private static String blockedQk(String media, String requestId, String traceId) {
+        String suffix = requestId != null && !requestId.isBlank() ? requestId : md5Hex(traceId == null ? media : traceId + "-" + Instant.now().toEpochMilli());
+        return "rta-" + media + "-" + suffix;
+    }
+
     private void requireVivoParams(Map<String, String> params) {
         if (firstParam(params, "accountId", "accountid") == null) {
             throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "missing vivo accountId");

+ 12 - 1
src/main/java/com/adx/tencent/vivo/model/VivoBidRecord.java

@@ -27,6 +27,7 @@ public class VivoBidRecord {
     private String creativeId;
     private String impId;
     private String tagId;
+    private Integer rtaCode;
     private Long price;
     private List<String> showUrls;
     private List<String> clickUrls;
@@ -58,6 +59,8 @@ public class VivoBidRecord {
     public void setImpId(String v) { this.impId = v; }
     public String getTagId() { return tagId; }
     public void setTagId(String v) { this.tagId = v; }
+    public Integer getRtaCode() { return rtaCode; }
+    public void setRtaCode(Integer v) { this.rtaCode = v; }
     public Long getPrice() { return price; }
     public void setPrice(Long v) { this.price = v == null ? 0L : v; }
     public List<String> getShowUrls() { return showUrls; }
@@ -84,6 +87,7 @@ public class VivoBidRecord {
         slim.platform = this.platform;
         slim.accountId = this.accountId;
         slim.tagId = this.tagId;
+        slim.rtaCode = this.rtaCode;
         slim.packageName = this.packageName;
         slim.mediaParams = slimDeviceParams(this.mediaParams);
         return slim;
@@ -97,6 +101,7 @@ public class VivoBidRecord {
         hot.platform = this.platform;
         hot.accountId = this.accountId;
         hot.tagId = this.tagId;
+        hot.rtaCode = this.rtaCode;
         hot.clickUrls = this.clickUrls;
         hot.landingPage = this.landingPage;
         hot.appStoreLink = this.appStoreLink;
@@ -114,6 +119,7 @@ public class VivoBidRecord {
         cold.accountId = this.accountId;
         cold.eventType = this.eventType;
         cold.tagId = this.tagId;
+        cold.rtaCode = this.rtaCode;
         cold.price = this.price == null ? 0L : this.price;
         cold.packageName = this.packageName;
         cold.mediaParams = slimDeviceParams(this.mediaParams);
@@ -138,7 +144,12 @@ public class VivoBidRecord {
                 "androidId",
                 "androidIdMD5",
                 "platform",
-                DEDUCTION_RATE_KEY
+                DEDUCTION_RATE_KEY,
+                "rta_tu",
+                "rta_order_id",
+                "rta_request_order_id",
+                "rta_request_id",
+                "rta_code"
         );
         Map<String, String> slim = new LinkedHashMap<>();
         for (String key : keepKeys) {

+ 5 - 0
src/main/java/com/adx/tencent/vivo/store/VivoColdStore.java

@@ -45,6 +45,7 @@ public class VivoColdStore {
                     platform VARCHAR(16) COMMENT '平台(android/ios)',
                     account_id VARCHAR(128) COMMENT 'vivo 投放账户ID',
                     event_type VARCHAR(32) COMMENT '事件类型(impression/click),仅保留排障所需',
+                    rta_code INT COMMENT 'RTA 状态码',
                     tag_id VARCHAR(128) NOT NULL COMMENT '百度广告位ID',
                     price BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '竞价价格(分)',
                     package_name VARCHAR(255) COMMENT '应用包名',
@@ -153,6 +154,10 @@ public class VivoColdStore {
             stmt.execute("UPDATE vivo_ad_bid_events SET price = 0 WHERE price IS NULL");
             stmt.execute("ALTER TABLE vivo_ad_bid_events MODIFY COLUMN price BIGINT UNSIGNED NOT NULL DEFAULT 0");
             try {
+                stmt.execute("ALTER TABLE vivo_ad_bid_events ADD COLUMN rta_code INT COMMENT 'RTA 状态码' AFTER event_type");
+            } catch (Exception ignored) {
+            }
+            try {
                 stmt.execute("ALTER TABLE vivo_media_callbacks ADD COLUMN deduction_rate INT NOT NULL DEFAULT 0 COMMENT '扣量比例(0-100)' AFTER purchase");
             } catch (Exception ignored) {
             }

+ 9 - 0
src/main/resources/application.yml

@@ -25,6 +25,15 @@ 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-tu-prefix: "huichuang1213"
+  honor-rta-order-ids: []
+  vivo-rta-order-ids: []
 
 baidu:
   clue:

+ 6 - 4
src/main/resources/mapper/HonorBidEventMapper.xml

@@ -10,6 +10,7 @@
         <result property="accountId" column="account_id"/>
         <result property="eventType" column="event_type"/>
         <result property="tagId" column="tag_id"/>
+        <result property="rtaCode" column="rta_code"/>
         <result property="price" column="price"/>
         <result property="packageName" column="package_name"/>
         <result property="mediaParams" column="media_params" typeHandler="com.adx.tencent.storage.typehandler.JsonStringMapTypeHandler"/>
@@ -18,7 +19,7 @@
 
     <select id="selectByQk" resultMap="honorBidRecordResultMap">
         SELECT qk, media, media_trace_id, platform, account_id, event_type,
-               tag_id, price, package_name, media_params, created_at
+               tag_id, rta_code, price, package_name, media_params, created_at
         FROM honor_ad_bid_events
         WHERE qk = #{qk}
         LIMIT 1
@@ -26,7 +27,7 @@
 
     <select id="selectByQks" resultMap="honorBidRecordResultMap">
         SELECT qk, media, media_trace_id, platform, account_id, event_type,
-               tag_id, price, package_name, media_params, created_at
+               tag_id, rta_code, price, package_name, media_params, created_at
         FROM honor_ad_bid_events
         WHERE qk IN
         <foreach collection="qks" item="qk" open="(" separator="," close=")">
@@ -37,12 +38,12 @@
     <insert id="insertOrUpdate">
         INSERT INTO honor_ad_bid_events (
             qk, media, media_trace_id, platform, account_id, event_type,
-            tag_id, price, package_name,
+            tag_id, rta_code, price, package_name,
             media_params, created_at
         ) VALUES (
             #{record.qk}, #{record.media}, #{record.mediaTraceId}, #{record.platform},
             #{record.accountId}, #{record.eventType}, #{record.tagId},
-            COALESCE(#{record.price}, 0), #{record.packageName},
+            #{record.rtaCode}, COALESCE(#{record.price}, 0), #{record.packageName},
             #{record.mediaParams, typeHandler=com.adx.tencent.storage.typehandler.JsonStringMapTypeHandler},
             #{record.createdAtTs}
         )
@@ -53,6 +54,7 @@
             account_id = VALUES(account_id),
             event_type = VALUES(event_type),
             tag_id = VALUES(tag_id),
+            rta_code = VALUES(rta_code),
             price = VALUES(price),
             package_name = VALUES(package_name),
             media_params = VALUES(media_params)

+ 6 - 4
src/main/resources/mapper/VivoBidEventMapper.xml

@@ -10,6 +10,7 @@
         <result property="accountId" column="account_id"/>
         <result property="eventType" column="event_type"/>
         <result property="tagId" column="tag_id"/>
+        <result property="rtaCode" column="rta_code"/>
         <result property="price" column="price"/>
         <result property="packageName" column="package_name"/>
         <result property="mediaParams" column="media_params" typeHandler="com.adx.tencent.storage.typehandler.JsonStringMapTypeHandler"/>
@@ -18,7 +19,7 @@
 
     <select id="selectByQk" resultMap="vivoBidRecordResultMap">
         SELECT qk, media, media_trace_id, platform, account_id, event_type,
-               tag_id, price, package_name, media_params, created_at
+               tag_id, rta_code, price, package_name, media_params, created_at
         FROM vivo_ad_bid_events
         WHERE qk = #{qk}
         LIMIT 1
@@ -26,7 +27,7 @@
 
     <select id="selectByQks" resultMap="vivoBidRecordResultMap">
         SELECT qk, media, media_trace_id, platform, account_id, event_type,
-               tag_id, price, package_name, media_params, created_at
+               tag_id, rta_code, price, package_name, media_params, created_at
         FROM vivo_ad_bid_events
         WHERE qk IN
         <foreach collection="qks" item="qk" open="(" separator="," close=")">
@@ -37,12 +38,12 @@
     <insert id="insertOrUpdate">
         INSERT INTO vivo_ad_bid_events (
             qk, media, media_trace_id, platform, account_id, event_type,
-            tag_id, price, package_name,
+            tag_id, rta_code, price, package_name,
             media_params, created_at
         ) VALUES (
             #{record.qk}, #{record.media}, #{record.mediaTraceId}, #{record.platform},
             #{record.accountId}, #{record.eventType}, #{record.tagId},
-            COALESCE(#{record.price}, 0), #{record.packageName},
+            #{record.rtaCode}, COALESCE(#{record.price}, 0), #{record.packageName},
             #{record.mediaParams, typeHandler=com.adx.tencent.storage.typehandler.JsonStringMapTypeHandler},
             #{record.createdAtTs}
         )
@@ -53,6 +54,7 @@
             account_id = VALUES(account_id),
             event_type = VALUES(event_type),
             tag_id = VALUES(tag_id),
+            rta_code = VALUES(rta_code),
             price = VALUES(price),
             package_name = VALUES(package_name),
             media_params = VALUES(media_params)