|
|
@@ -0,0 +1,340 @@
|
|
|
+package com.adx.tencent.oppo.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.config.AppProperties;
|
|
|
+import com.adx.tencent.oppo.model.OppoBidRecord;
|
|
|
+import com.adx.tencent.oppo.model.OppoTrackingRecord;
|
|
|
+import com.adx.tencent.oppo.store.OppoHotStore;
|
|
|
+import com.adx.tencent.rta.BaiduRtaDecision;
|
|
|
+import com.adx.tencent.rta.BaiduRtaService;
|
|
|
+import jakarta.servlet.http.HttpServletRequest;
|
|
|
+import jakarta.servlet.http.HttpServletResponse;
|
|
|
+import org.springframework.http.HttpStatus;
|
|
|
+import org.springframework.lang.Nullable;
|
|
|
+import org.springframework.web.bind.annotation.GetMapping;
|
|
|
+import org.springframework.web.bind.annotation.RestController;
|
|
|
+import org.springframework.web.server.ResponseStatusException;
|
|
|
+
|
|
|
+import java.time.Instant;
|
|
|
+import java.util.*;
|
|
|
+import java.util.concurrent.Semaphore;
|
|
|
+
|
|
|
+/**
|
|
|
+ * OPPO 监测流量入口:OPPO -> 本服务 -> 百度 ADX。
|
|
|
+ */
|
|
|
+@RestController
|
|
|
+public class OppoTrackingController {
|
|
|
+ private final AdxClient adx;
|
|
|
+ private final OppoHotStore store;
|
|
|
+ private final AppProperties props;
|
|
|
+ private final BaiduRtaService rtaService;
|
|
|
+ private final Semaphore impression = new Semaphore(50), click = new Semaphore(50);
|
|
|
+
|
|
|
+ public OppoTrackingController(AdxClient adx, @Nullable OppoHotStore store, AppProperties props,
|
|
|
+ @Nullable BaiduRtaService rtaService) {
|
|
|
+ this.adx = adx;
|
|
|
+ this.store = store;
|
|
|
+ this.props = props;
|
|
|
+ this.rtaService = rtaService;
|
|
|
+ }
|
|
|
+
|
|
|
+ @GetMapping("/oppo/impression")
|
|
|
+ public Object impression(HttpServletRequest request) throws Exception {
|
|
|
+ impression.acquire();
|
|
|
+ try {
|
|
|
+ return handleImpression(request);
|
|
|
+ } finally {
|
|
|
+ impression.release();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @GetMapping("/oppo/click")
|
|
|
+ public Object click(HttpServletRequest request, HttpServletResponse response) throws Exception {
|
|
|
+ click.acquire();
|
|
|
+ try {
|
|
|
+ return handleClick(request, response);
|
|
|
+ } finally {
|
|
|
+ click.release();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private Object handleImpression(HttpServletRequest request) throws Exception {
|
|
|
+ Map<String, String> p = params(request);
|
|
|
+ String trace = required(p, "req", "requestId");
|
|
|
+ ensureReady();
|
|
|
+ applyResolvedDefaults(p);
|
|
|
+ BaiduRtaDecision rtaDecision = evaluateRta(trace, p, request);
|
|
|
+ Map<String, String> rtaParams = enrichMediaParams(p, rtaDecision);
|
|
|
+ if (!rtaDecision.allowsBid()) {
|
|
|
+ OppoBidRecord blocked = blockedBidRecord(trace, "impression", rtaParams, rtaDecision);
|
|
|
+ store.recordBid(blocked);
|
|
|
+ return responseBody(rtaParams, blocked.getQk(), trace, blocked.getTagId(), rtaDecision);
|
|
|
+ }
|
|
|
+ NormalizedBidResponse response = adx.requestBid("oppo", trace, buildBidRequest(request, trace, rtaParams));
|
|
|
+ if (response == null) throw bad("empty baidu response");
|
|
|
+ OppoBidRecord record = record(response, trace, "impression", rtaParams);
|
|
|
+ record.setRtaCode(rtaDecision.getCode());
|
|
|
+ store.recordBid(record);
|
|
|
+ report(record, response.getBid() == null ? null : response.getBid().getShowUrls(), "impression");
|
|
|
+ return responseBody(rtaParams, record.getQk(), trace, record.getTagId(), rtaDecision);
|
|
|
+ }
|
|
|
+
|
|
|
+ private Object handleClick(HttpServletRequest request, HttpServletResponse response) throws Exception {
|
|
|
+ Map<String, String> p = params(request);
|
|
|
+ String trace = required(p, "req", "requestId");
|
|
|
+ ensureReady();
|
|
|
+ OppoBidRecord record = store.findBidByMediaTrace(trace);
|
|
|
+ BaiduRtaDecision rtaDecision = null;
|
|
|
+ if (record == null) {
|
|
|
+ applyResolvedDefaults(p);
|
|
|
+ rtaDecision = evaluateRta(trace, p, request);
|
|
|
+ Map<String, String> rtaParams = enrichMediaParams(p, rtaDecision);
|
|
|
+ if (!rtaDecision.allowsBid()) {
|
|
|
+ OppoBidRecord blocked = blockedBidRecord(trace, "click", rtaParams, rtaDecision);
|
|
|
+ store.recordBid(blocked);
|
|
|
+ return responseBody(rtaParams, blocked.getQk(), trace, blocked.getTagId(), rtaDecision);
|
|
|
+ }
|
|
|
+ NormalizedBidResponse bid = adx.requestBid("oppo", trace, buildBidRequest(request, trace, rtaParams));
|
|
|
+ if (bid == null) throw bad("empty baidu response");
|
|
|
+ record = record(bid, trace, "click", rtaParams);
|
|
|
+ record.setRtaCode(rtaDecision.getCode());
|
|
|
+ store.recordBid(record);
|
|
|
+ report(record, bid.getBid() == null ? null : bid.getBid().getShowUrls(), "impression");
|
|
|
+ }
|
|
|
+ report(record, record.getClickUrls(), "click");
|
|
|
+ try {
|
|
|
+ store.shrinkAfterClick(record);
|
|
|
+ } catch (Exception ignored) {
|
|
|
+ // 与其他渠道一致,缓存瘦身失败不阻断已完成上报的点击跳转。
|
|
|
+ }
|
|
|
+ String destination = record.getAppStoreLink();
|
|
|
+ if (destination == null || destination.isBlank()) destination = record.getLandingPage();
|
|
|
+ if (destination != null && !destination.isBlank()) {
|
|
|
+ response.sendRedirect(destination);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ return responseBody(p, record.getQk(), trace, record.getTagId(), rtaDecision);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void report(OppoBidRecord r, List<String> urls, String kind) {
|
|
|
+ if (urls == null) return;
|
|
|
+ try {
|
|
|
+ List<TrackingResult> results = "impression".equals(kind) ? adx.reportImpression(urls, r.getPrice() == null ? 0 : r.getPrice()) : adx.reportClick(urls);
|
|
|
+ for (TrackingResult x : results) {
|
|
|
+ OppoTrackingRecord t = new OppoTrackingRecord();
|
|
|
+ t.setQk(r.getQk());
|
|
|
+ t.setTagId(r.getTagId());
|
|
|
+ t.setKind(kind);
|
|
|
+ t.setUrl(x.getUrl());
|
|
|
+ t.setStatus(x.getStatus());
|
|
|
+ t.setOk(x.isOk());
|
|
|
+ t.setCreatedAt(Instant.now());
|
|
|
+ if (store != null) store.recordTracking(t);
|
|
|
+ }
|
|
|
+ } catch (Exception ignored) {
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private OppoBidRecord record(NormalizedBidResponse response, String trace, String kind, Map<String, String> p) {
|
|
|
+ OppoBidRecord r = new OppoBidRecord();
|
|
|
+ r.setQk(response.getQk());
|
|
|
+ r.setMediaTraceId(trace);
|
|
|
+ r.setPlatform(resolvePlatform(p));
|
|
|
+ r.setAccountId(first(p, "ownerId", "owner_id"));
|
|
|
+ r.setTagId(first(p, "tagId", "tag_id", "baidu_tag_id"));
|
|
|
+ r.setEventType(kind);
|
|
|
+ r.setMediaParams(p);
|
|
|
+ r.setCreatedAt(Instant.now());
|
|
|
+ if (response.getBid() != null) {
|
|
|
+ r.setPrice(response.getBid().getPrice());
|
|
|
+ r.setShowUrls(response.getBid().getShowUrls());
|
|
|
+ r.setClickUrls(response.getBid().getClickUrls());
|
|
|
+ r.setLandingPage(response.getBid().getLandingPage());
|
|
|
+ r.setAppStoreLink(response.getBid().getAppStoreLink());
|
|
|
+ r.setTagId(first(response.getBid().getTagId(), r.getTagId()));
|
|
|
+ if (response.getBid().getAdm() != null) r.setPackageName(response.getBid().getAdm().getPackageName());
|
|
|
+ }
|
|
|
+ r.setPackageName(firstValue(r.getPackageName(), p.get("pkg"), p.get("packageName")));
|
|
|
+ return r;
|
|
|
+ }
|
|
|
+
|
|
|
+ private BaiduRtaDecision evaluateRta(String trace, Map<String, String> p, HttpServletRequest request) {
|
|
|
+ if (rtaService == null) return BaiduRtaDecision.bypass();
|
|
|
+ return rtaService.evaluate(
|
|
|
+ "oppo",
|
|
|
+ resolveBaiduAppId(p),
|
|
|
+ resolveTagId(p),
|
|
|
+ resolvePlatform(p),
|
|
|
+ p,
|
|
|
+ firstValue(first(p, "ip", "clientIp", "client_ip"), clientIp(request, p)),
|
|
|
+ normalizeUserAgent(first(p, "ua", "user_agent", "useragent"), resolvePlatform(p)));
|
|
|
+ }
|
|
|
+
|
|
|
+ private OppoBidRecord blockedBidRecord(String trace, String kind, Map<String, String> p, BaiduRtaDecision decision) {
|
|
|
+ OppoBidRecord r = new OppoBidRecord();
|
|
|
+ r.setQk(firstValue(decision.getQk(), decision.getRequestId(), "oppo-rta-blocked-" + UUID.randomUUID().toString().replace("-", "")));
|
|
|
+ r.setMediaTraceId(trace);
|
|
|
+ r.setPlatform(resolvePlatform(p));
|
|
|
+ r.setAccountId(first(p, "ownerId", "owner_id"));
|
|
|
+ r.setTagId(value(resolveTagId(p)));
|
|
|
+ r.setEventType(kind);
|
|
|
+ r.setRtaCode(decision.getCode());
|
|
|
+ r.setPrice(0L);
|
|
|
+ r.setPackageName(first(p, "pkg", "packageName"));
|
|
|
+ r.setMediaParams(enrichMediaParams(p, decision));
|
|
|
+ r.setCreatedAt(Instant.now());
|
|
|
+ return r;
|
|
|
+ }
|
|
|
+
|
|
|
+ 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", firstValue(decision.getOrderIds(), decision.getRequestOrderIds()));
|
|
|
+ putIfNotBlank(enriched, "rta_request_order_id", decision.getRequestOrderIds());
|
|
|
+ putIfNotBlank(enriched, "rta_qk", decision.getQk());
|
|
|
+ putIfNotBlank(enriched, "rta_request_id", decision.getRequestId());
|
|
|
+ if (decision.getCode() != null) enriched.put("rta_code", String.valueOf(decision.getCode()));
|
|
|
+ return enriched;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static Map<String, Object> responseBody(Map<String, String> params,
|
|
|
+ String qk,
|
|
|
+ String trace,
|
|
|
+ String tagId,
|
|
|
+ BaiduRtaDecision decision) {
|
|
|
+ Map<String, Object> body = new LinkedHashMap<>();
|
|
|
+ body.put("qk", value(qk));
|
|
|
+ body.put("req", trace);
|
|
|
+ body.put("tagId", value(tagId));
|
|
|
+ if ("true".equalsIgnoreCase(firstValue(params.get("_debugRta"), params.get("debugRta")))) {
|
|
|
+ body.put("rtaApplied", decision != null && decision.isApplied());
|
|
|
+ body.put("rtaMatched", decision == null ? null : decision.isMatched());
|
|
|
+ body.put("rtaCode", decision == null ? null : decision.getCode());
|
|
|
+ body.put("rtaQk", decision == null ? null : decision.getQk());
|
|
|
+ body.put("rtaRequestId", decision == null ? null : decision.getRequestId());
|
|
|
+ body.put("rtaRequestOrderId", decision == null ? null : decision.getRequestOrderIds());
|
|
|
+ body.put("rtaOrderId", decision == null ? null : decision.getOrderIds());
|
|
|
+ body.put("rtaError", decision == null ? null : decision.getErrorMessage());
|
|
|
+ }
|
|
|
+ return body;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<String, Object> buildBidRequest(HttpServletRequest req, String trace, Map<String, String> p) {
|
|
|
+ String app = resolveBaiduAppId(p);
|
|
|
+ String tag = resolveTagId(p);
|
|
|
+ Map<String, Object> imp = new LinkedHashMap<>();
|
|
|
+ imp.put("id", "1");
|
|
|
+ imp.put("appId", app);
|
|
|
+ imp.put("tagId", tag);
|
|
|
+ imp.put("secure", 1);
|
|
|
+ imp.put("adType", 0);
|
|
|
+ imp.put("maxCount", 1);
|
|
|
+ imp.put("assets", List.of(Map.of("type", 1)));
|
|
|
+ Map<String, Object> device = new LinkedHashMap<>();
|
|
|
+ device.put("ua", firstValue(first(p, "ua", "user_agent"), req.getHeader("User-Agent")));
|
|
|
+ device.put("deviceType", 1);
|
|
|
+ device.put("ip", clientIp(req, p));
|
|
|
+ if (first(p, "oaid") != null) device.put("uid", Map.of("oaid", first(p, "oaid")));
|
|
|
+ Map<String, Object> body = new LinkedHashMap<>();
|
|
|
+ body.put("mediaId", props.getBaiduMediaId());
|
|
|
+ body.put("reqId", trace);
|
|
|
+ body.put("imp", List.of(imp));
|
|
|
+ body.put("device", device);
|
|
|
+ return body;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static String clientIp(HttpServletRequest request, Map<String, String> params) {
|
|
|
+ String parameterIp = first(params, "ip", "clientIp", "client_ip");
|
|
|
+ if (parameterIp != null) return parameterIp;
|
|
|
+ String forwarded = request.getHeader("X-Forwarded-For");
|
|
|
+ if (forwarded != null && !forwarded.isBlank()) return forwarded.split(",", 2)[0].trim();
|
|
|
+ String real = request.getHeader("X-Real-IP");
|
|
|
+ return real == null || real.isBlank() ? request.getRemoteAddr() : real.trim();
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<String, String> params(HttpServletRequest r) {
|
|
|
+ Map<String, String> p = new LinkedHashMap<>();
|
|
|
+ r.getParameterMap().forEach((k, v) -> {
|
|
|
+ if (v != null && v.length > 0 && v[0] != null) p.put(k, v[0].trim());
|
|
|
+ });
|
|
|
+ return p;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void applyResolvedDefaults(Map<String, String> p) {
|
|
|
+ p.put("platform", resolvePlatform(p));
|
|
|
+ String tag = resolveTagId(p);
|
|
|
+ if (tag != null) p.put("tagId", tag);
|
|
|
+ String app = resolveBaiduAppId(p);
|
|
|
+ if (app != null) p.put("baidu_app_id", app);
|
|
|
+ }
|
|
|
+
|
|
|
+ private String resolveBaiduAppId(Map<String, String> p) {
|
|
|
+ return firstValue(first(p, "baidu_app_id"), props.getBaiduAppId());
|
|
|
+ }
|
|
|
+
|
|
|
+ private String resolveTagId(Map<String, String> p) {
|
|
|
+ return firstValue(first(p, "tagId", "tag_id", "baidu_tag_id"), props.getBaiduTagId());
|
|
|
+ }
|
|
|
+
|
|
|
+ private static String resolvePlatform(Map<String, String> p) {
|
|
|
+ String platform = first(p, "platform", "os");
|
|
|
+ if (platform == null) return "android";
|
|
|
+ String normalized = platform.trim().toLowerCase(Locale.ROOT);
|
|
|
+ if ("ios".equals(normalized)) return "ios";
|
|
|
+ if ("harmony".equals(normalized) || "hongmeng".equals(normalized)) return "harmony";
|
|
|
+ return "android";
|
|
|
+ }
|
|
|
+
|
|
|
+ private void ensureReady() {
|
|
|
+ if (store == null)
|
|
|
+ throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "oppo store not configured");
|
|
|
+ }
|
|
|
+
|
|
|
+ private static String required(Map<String, String> p, String... keys) {
|
|
|
+ String v = first(p, keys);
|
|
|
+ if (v == null || v.isBlank())
|
|
|
+ throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "missing OPPO request id");
|
|
|
+ return v;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static String first(Map<String, String> p, String... k) {
|
|
|
+ for (String x : k) {
|
|
|
+ String v = p.get(x);
|
|
|
+ if (v != null && !v.isBlank()) return v;
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static String first(String a, String b) {
|
|
|
+ return a != null && !a.isBlank() ? a : b;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static String firstValue(String... values) {
|
|
|
+ for (String v : values) if (v != null && !v.isBlank()) return v;
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static void putIfNotBlank(Map<String, String> params, String key, String value) {
|
|
|
+ if (value != null && !value.isBlank()) params.put(key, value);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static String normalizeUserAgent(String ua, String platform) {
|
|
|
+ if (ua != null && !ua.isBlank()) return ua.trim();
|
|
|
+ if ("ios".equals(platform)) {
|
|
|
+ return "Mozilla/5.0 (iPhone; CPU iPhone OS 17_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1";
|
|
|
+ }
|
|
|
+ return "Mozilla/5.0 (Linux; Android 14; Pixel 8 Build/UQ1A.240205.002) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.71 Mobile Safari/537.36";
|
|
|
+ }
|
|
|
+
|
|
|
+ private static String value(String v) {
|
|
|
+ return v == null ? "" : v;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static ResponseStatusException bad(String s) {
|
|
|
+ return new ResponseStatusException(HttpStatus.BAD_GATEWAY, s);
|
|
|
+ }
|
|
|
+}
|