|
|
@@ -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) {
|
|
|
+ }
|
|
|
+}
|