OppoTrackingController.java 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  1. package com.adx.tencent.oppo.controller;
  2. import com.adx.tencent.baidu.AdxClient;
  3. import com.adx.tencent.baidu.model.NormalizedBidResponse;
  4. import com.adx.tencent.baidu.model.TrackingResult;
  5. import com.adx.tencent.config.AppProperties;
  6. import com.adx.tencent.oppo.model.OppoBidRecord;
  7. import com.adx.tencent.oppo.model.OppoTrackingRecord;
  8. import com.adx.tencent.oppo.store.OppoHotStore;
  9. import com.adx.tencent.rta.BaiduRtaDecision;
  10. import com.adx.tencent.rta.BaiduRtaService;
  11. import jakarta.servlet.http.HttpServletRequest;
  12. import jakarta.servlet.http.HttpServletResponse;
  13. import org.springframework.http.HttpStatus;
  14. import org.springframework.lang.Nullable;
  15. import org.springframework.web.bind.annotation.GetMapping;
  16. import org.springframework.web.bind.annotation.RestController;
  17. import org.springframework.web.server.ResponseStatusException;
  18. import java.time.Instant;
  19. import java.util.*;
  20. import java.util.concurrent.Semaphore;
  21. /**
  22. * OPPO 监测流量入口:OPPO -> 本服务 -> 百度 ADX。
  23. */
  24. @RestController
  25. public class OppoTrackingController {
  26. private final AdxClient adx;
  27. private final OppoHotStore store;
  28. private final AppProperties props;
  29. private final BaiduRtaService rtaService;
  30. private final Semaphore impression = new Semaphore(50), click = new Semaphore(50);
  31. public OppoTrackingController(AdxClient adx, @Nullable OppoHotStore store, AppProperties props,
  32. @Nullable BaiduRtaService rtaService) {
  33. this.adx = adx;
  34. this.store = store;
  35. this.props = props;
  36. this.rtaService = rtaService;
  37. }
  38. @GetMapping("/oppo/impression")
  39. public Object impression(HttpServletRequest request) throws Exception {
  40. impression.acquire();
  41. try {
  42. return handleImpression(request);
  43. } finally {
  44. impression.release();
  45. }
  46. }
  47. @GetMapping("/oppo/click")
  48. public Object click(HttpServletRequest request, HttpServletResponse response) throws Exception {
  49. click.acquire();
  50. try {
  51. return handleClick(request, response);
  52. } finally {
  53. click.release();
  54. }
  55. }
  56. private Object handleImpression(HttpServletRequest request) throws Exception {
  57. Map<String, String> p = params(request);
  58. String trace = required(p, "req", "requestId");
  59. ensureReady();
  60. applyResolvedDefaults(p);
  61. BaiduRtaDecision rtaDecision = evaluateRta(trace, p, request);
  62. Map<String, String> rtaParams = enrichMediaParams(p, rtaDecision);
  63. if (!rtaDecision.allowsBid()) {
  64. OppoBidRecord blocked = blockedBidRecord(trace, "impression", rtaParams, rtaDecision);
  65. saveBlockedBid(blocked);
  66. return responseBody(rtaParams, blocked.getQk(), trace, blocked.getTagId(), rtaDecision);
  67. }
  68. NormalizedBidResponse response = adx.requestBid("oppo", trace, buildBidRequest(request, trace, rtaParams, rtaDecision));
  69. if (response == null) throw bad("empty baidu response");
  70. OppoBidRecord record = record(response, trace, "impression", rtaParams);
  71. record.setRtaCode(rtaDecision.getCode());
  72. store.recordBid(record);
  73. report(record, response.getBid() == null ? null : response.getBid().getShowUrls(), "impression");
  74. return responseBody(rtaParams, record.getQk(), trace, record.getTagId(), rtaDecision);
  75. }
  76. private Object handleClick(HttpServletRequest request, HttpServletResponse response) throws Exception {
  77. Map<String, String> p = params(request);
  78. String trace = required(p, "req", "requestId");
  79. ensureReady();
  80. OppoBidRecord record = store.findBidByMediaTrace(trace);
  81. BaiduRtaDecision rtaDecision = null;
  82. if (record == null) {
  83. applyResolvedDefaults(p);
  84. rtaDecision = evaluateRta(trace, p, request);
  85. Map<String, String> rtaParams = enrichMediaParams(p, rtaDecision);
  86. if (!rtaDecision.allowsBid()) {
  87. OppoBidRecord blocked = blockedBidRecord(trace, "click", rtaParams, rtaDecision);
  88. saveBlockedBid(blocked);
  89. return responseBody(rtaParams, blocked.getQk(), trace, blocked.getTagId(), rtaDecision);
  90. }
  91. NormalizedBidResponse bid = adx.requestBid("oppo", trace, buildBidRequest(request, trace, rtaParams, rtaDecision));
  92. if (bid == null) throw bad("empty baidu response");
  93. record = record(bid, trace, "click", rtaParams);
  94. record.setRtaCode(rtaDecision.getCode());
  95. store.recordBid(record);
  96. report(record, bid.getBid() == null ? null : bid.getBid().getShowUrls(), "impression");
  97. }
  98. report(record, record.getClickUrls(), "click");
  99. try {
  100. store.shrinkAfterClick(record);
  101. } catch (Exception ignored) {
  102. // 与其他渠道一致,缓存瘦身失败不阻断已完成上报的点击跳转。
  103. }
  104. String destination = record.getAppStoreLink();
  105. if (destination == null || destination.isBlank()) destination = record.getLandingPage();
  106. if (destination != null && !destination.isBlank()) {
  107. response.sendRedirect(destination);
  108. return null;
  109. }
  110. return responseBody(p, record.getQk(), trace, record.getTagId(), rtaDecision);
  111. }
  112. private void report(OppoBidRecord r, List<String> urls, String kind) {
  113. if (urls == null) return;
  114. try {
  115. List<TrackingResult> results = "impression".equals(kind) ? adx.reportImpression(urls, r.getPrice() == null ? 0 : r.getPrice()) : adx.reportClick(urls);
  116. for (TrackingResult x : results) {
  117. OppoTrackingRecord t = new OppoTrackingRecord();
  118. t.setQk(r.getQk());
  119. t.setTagId(r.getTagId());
  120. t.setKind(kind);
  121. t.setUrl(x.getUrl());
  122. t.setStatus(x.getStatus());
  123. t.setOk(x.isOk());
  124. t.setCreatedAt(Instant.now());
  125. if (store != null) store.recordTracking(t);
  126. }
  127. } catch (Exception ignored) {
  128. }
  129. }
  130. private OppoBidRecord record(NormalizedBidResponse response, String trace, String kind, Map<String, String> p) {
  131. OppoBidRecord r = new OppoBidRecord();
  132. r.setQk(response.getQk());
  133. r.setMediaTraceId(trace);
  134. r.setPlatform(resolvePlatform(p));
  135. r.setAccountId(first(p, "ownerId", "owner_id"));
  136. r.setTagId(first(p, "tagId", "tag_id", "baidu_tag_id"));
  137. r.setEventType(kind);
  138. r.setMediaParams(p);
  139. r.setCreatedAt(Instant.now());
  140. if (response.getBid() != null) {
  141. r.setPrice(response.getBid().getPrice());
  142. r.setShowUrls(response.getBid().getShowUrls());
  143. r.setClickUrls(response.getBid().getClickUrls());
  144. r.setLandingPage(response.getBid().getLandingPage());
  145. r.setAppStoreLink(response.getBid().getAppStoreLink());
  146. r.setTagId(first(response.getBid().getTagId(), r.getTagId()));
  147. if (response.getBid().getAdm() != null) r.setPackageName(response.getBid().getAdm().getPackageName());
  148. }
  149. r.setPackageName(firstValue(r.getPackageName(), p.get("pkg"), p.get("packageName")));
  150. return r;
  151. }
  152. private BaiduRtaDecision evaluateRta(String trace, Map<String, String> p, HttpServletRequest request) {
  153. if (rtaService == null) return BaiduRtaDecision.bypass();
  154. return rtaService.evaluate(
  155. "oppo",
  156. resolveBaiduAppId(p),
  157. resolveTagId(p),
  158. resolvePlatform(p),
  159. p,
  160. firstValue(first(p, "ip", "clientIp", "client_ip"), clientIp(request, p)),
  161. normalizeUserAgent(first(p, "ua", "user_agent", "useragent"), resolvePlatform(p)));
  162. }
  163. private OppoBidRecord blockedBidRecord(String trace, String kind, Map<String, String> p, BaiduRtaDecision decision) {
  164. OppoBidRecord r = new OppoBidRecord();
  165. r.setQk(firstValue(decision.getQk(), decision.getRequestId(), "oppo-rta-blocked-" + UUID.randomUUID().toString().replace("-", "")));
  166. r.setMediaTraceId(trace);
  167. r.setPlatform(resolvePlatform(p));
  168. r.setAccountId(first(p, "ownerId", "owner_id"));
  169. r.setTagId(value(resolveTagId(p)));
  170. r.setEventType(kind);
  171. r.setRtaCode(decision.getCode());
  172. r.setPrice(0L);
  173. r.setPackageName(first(p, "pkg", "packageName"));
  174. r.setMediaParams(enrichMediaParams(p, decision));
  175. r.setCreatedAt(Instant.now());
  176. return r;
  177. }
  178. private static Map<String, String> enrichMediaParams(Map<String, String> params, BaiduRtaDecision decision) {
  179. Map<String, String> enriched = new LinkedHashMap<>();
  180. if (params != null) enriched.putAll(params);
  181. if (decision == null || !decision.isApplied()) return enriched;
  182. putIfNotBlank(enriched, "rta_tu", decision.getTu());
  183. putIfNotBlank(enriched, "rta_order_id", firstValue(decision.getOrderIds(), decision.getRequestOrderIds()));
  184. putIfNotBlank(enriched, "rta_request_order_id", decision.getRequestOrderIds());
  185. putIfNotBlank(enriched, "rta_qk", decision.getQk());
  186. putIfNotBlank(enriched, "rta_request_id", decision.getRequestId());
  187. if (decision.getCode() != null) enriched.put("rta_code", String.valueOf(decision.getCode()));
  188. return enriched;
  189. }
  190. private void saveBlockedBid(OppoBidRecord record) {
  191. store.recordBlockedBid(record);
  192. }
  193. private static Map<String, Object> responseBody(Map<String, String> params,
  194. String qk,
  195. String trace,
  196. String tagId,
  197. BaiduRtaDecision decision) {
  198. Map<String, Object> body = new LinkedHashMap<>();
  199. body.put("qk", value(qk));
  200. body.put("req", trace);
  201. body.put("tagId", value(tagId));
  202. if ("true".equalsIgnoreCase(firstValue(params.get("_debugRta"), params.get("debugRta")))) {
  203. body.put("rtaApplied", decision != null && decision.isApplied());
  204. body.put("rtaMatched", decision == null ? null : decision.isMatched());
  205. body.put("rtaCode", decision == null ? null : decision.getCode());
  206. body.put("rtaQk", decision == null ? null : decision.getQk());
  207. body.put("rtaRequestId", decision == null ? null : decision.getRequestId());
  208. body.put("rtaRequestOrderId", decision == null ? null : decision.getRequestOrderIds());
  209. body.put("rtaOrderId", decision == null ? null : decision.getOrderIds());
  210. body.put("rtaError", decision == null ? null : decision.getErrorMessage());
  211. }
  212. return body;
  213. }
  214. private Map<String, Object> buildBidRequest(HttpServletRequest req,
  215. String trace,
  216. Map<String, String> p,
  217. BaiduRtaDecision rtaDecision) {
  218. String app = resolveBaiduAppId(p);
  219. String tag = resolveTagId(p);
  220. Map<String, Object> imp = new LinkedHashMap<>();
  221. imp.put("id", "1");
  222. imp.put("appId", app);
  223. imp.put("tagId", tag);
  224. imp.put("secure", 1);
  225. imp.put("adType", 0);
  226. imp.put("maxCount", 1);
  227. imp.put("assets", List.of(Map.of("type", 1)));
  228. addRtaExtTransData(imp, rtaDecision);
  229. Map<String, Object> device = new LinkedHashMap<>();
  230. device.put("ua", firstValue(first(p, "ua", "user_agent"), req.getHeader("User-Agent")));
  231. device.put("deviceType", 1);
  232. device.put("ip", clientIp(req, p));
  233. if (first(p, "oaid") != null) device.put("uid", Map.of("oaid", first(p, "oaid")));
  234. Map<String, Object> body = new LinkedHashMap<>();
  235. body.put("mediaId", props.getBaiduMediaId());
  236. body.put("reqId", trace);
  237. body.put("imp", List.of(imp));
  238. body.put("device", device);
  239. return body;
  240. }
  241. private static void addRtaExtTransData(Map<String, Object> imp, BaiduRtaDecision decision) {
  242. String orderId = rtaOrderId(decision);
  243. if (orderId == null) {
  244. return;
  245. }
  246. Map<String, Object> extTransData = new LinkedHashMap<>();
  247. extTransData.put("key", "order_id");
  248. extTransData.put("value", orderId);
  249. imp.put("ext_trans_data", List.of(extTransData));
  250. }
  251. private static String rtaOrderId(BaiduRtaDecision decision) {
  252. if (decision == null || !decision.isApplied() || !decision.isMatched()) {
  253. return null;
  254. }
  255. return firstValue(decision.getOrderIds(), decision.getRequestOrderIds());
  256. }
  257. private static String clientIp(HttpServletRequest request, Map<String, String> params) {
  258. String parameterIp = first(params, "ip", "clientIp", "client_ip");
  259. if (parameterIp != null) return parameterIp;
  260. String forwarded = request.getHeader("X-Forwarded-For");
  261. if (forwarded != null && !forwarded.isBlank()) return forwarded.split(",", 2)[0].trim();
  262. String real = request.getHeader("X-Real-IP");
  263. return real == null || real.isBlank() ? request.getRemoteAddr() : real.trim();
  264. }
  265. private Map<String, String> params(HttpServletRequest r) {
  266. Map<String, String> p = new LinkedHashMap<>();
  267. r.getParameterMap().forEach((k, v) -> {
  268. if (v != null && v.length > 0 && v[0] != null) p.put(k, v[0].trim());
  269. });
  270. return p;
  271. }
  272. private void applyResolvedDefaults(Map<String, String> p) {
  273. p.put("platform", resolvePlatform(p));
  274. String tag = resolveTagId(p);
  275. if (tag != null) p.put("tagId", tag);
  276. String app = resolveBaiduAppId(p);
  277. if (app != null) p.put("baidu_app_id", app);
  278. }
  279. private String resolveBaiduAppId(Map<String, String> p) {
  280. return firstValue(first(p, "baidu_app_id"), props.getBaiduAppId());
  281. }
  282. private String resolveTagId(Map<String, String> p) {
  283. return firstValue(first(p, "tagId", "tag_id", "baidu_tag_id"), props.getBaiduTagId());
  284. }
  285. private static String resolvePlatform(Map<String, String> p) {
  286. String platform = first(p, "platform", "os");
  287. if (platform == null) return "android";
  288. String normalized = platform.trim().toLowerCase(Locale.ROOT);
  289. if ("ios".equals(normalized)) return "ios";
  290. if ("harmony".equals(normalized) || "hongmeng".equals(normalized)) return "harmony";
  291. return "android";
  292. }
  293. private void ensureReady() {
  294. if (store == null)
  295. throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "oppo store not configured");
  296. }
  297. private static String required(Map<String, String> p, String... keys) {
  298. String v = first(p, keys);
  299. if (v == null || v.isBlank())
  300. throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "missing OPPO request id");
  301. return v;
  302. }
  303. private static String first(Map<String, String> p, String... k) {
  304. for (String x : k) {
  305. String v = p.get(x);
  306. if (v != null && !v.isBlank()) return v;
  307. }
  308. return null;
  309. }
  310. private static String first(String a, String b) {
  311. return a != null && !a.isBlank() ? a : b;
  312. }
  313. private static String firstValue(String... values) {
  314. for (String v : values) if (v != null && !v.isBlank()) return v;
  315. return null;
  316. }
  317. private static void putIfNotBlank(Map<String, String> params, String key, String value) {
  318. if (value != null && !value.isBlank()) params.put(key, value);
  319. }
  320. private static String normalizeUserAgent(String ua, String platform) {
  321. if (ua != null && !ua.isBlank()) return ua.trim();
  322. if ("ios".equals(platform)) {
  323. 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";
  324. }
  325. 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";
  326. }
  327. private static String value(String v) {
  328. return v == null ? "" : v;
  329. }
  330. private static ResponseStatusException bad(String s) {
  331. return new ResponseStatusException(HttpStatus.BAD_GATEWAY, s);
  332. }
  333. }