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 p = params(request); String trace = required(p, "req", "requestId"); ensureReady(); applyResolvedDefaults(p); BaiduRtaDecision rtaDecision = evaluateRta(trace, p, request); Map rtaParams = enrichMediaParams(p, rtaDecision); if (!rtaDecision.allowsBid()) { OppoBidRecord blocked = blockedBidRecord(trace, "impression", rtaParams, rtaDecision); saveBlockedBid(blocked); return responseBody(rtaParams, blocked.getQk(), trace, blocked.getTagId(), rtaDecision); } NormalizedBidResponse response = adx.requestBid("oppo", trace, buildBidRequest(request, trace, rtaParams, rtaDecision)); 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 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 rtaParams = enrichMediaParams(p, rtaDecision); if (!rtaDecision.allowsBid()) { OppoBidRecord blocked = blockedBidRecord(trace, "click", rtaParams, rtaDecision); saveBlockedBid(blocked); return responseBody(rtaParams, blocked.getQk(), trace, blocked.getTagId(), rtaDecision); } NormalizedBidResponse bid = adx.requestBid("oppo", trace, buildBidRequest(request, trace, rtaParams, rtaDecision)); 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 urls, String kind) { if (urls == null) return; try { List 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 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 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 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 enrichMediaParams(Map params, BaiduRtaDecision decision) { Map 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 void saveBlockedBid(OppoBidRecord record) { store.recordBlockedBid(record); } private static Map responseBody(Map params, String qk, String trace, String tagId, BaiduRtaDecision decision) { Map 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 buildBidRequest(HttpServletRequest req, String trace, Map p, BaiduRtaDecision rtaDecision) { String app = resolveBaiduAppId(p); String tag = resolveTagId(p); Map 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))); addRtaExtTransData(imp, rtaDecision); Map 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 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 void addRtaExtTransData(Map imp, BaiduRtaDecision decision) { String orderId = rtaOrderId(decision); if (orderId == null) { return; } Map extTransData = new LinkedHashMap<>(); extTransData.put("key", "order_id"); extTransData.put("value", orderId); imp.put("ext_trans_data", List.of(extTransData)); } private static String rtaOrderId(BaiduRtaDecision decision) { if (decision == null || !decision.isApplied() || !decision.isMatched()) { return null; } return firstValue(decision.getOrderIds(), decision.getRequestOrderIds()); } private static String clientIp(HttpServletRequest request, Map 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 params(HttpServletRequest r) { Map 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 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 p) { return firstValue(first(p, "baidu_app_id"), props.getBaiduAppId()); } private String resolveTagId(Map p) { return firstValue(first(p, "tagId", "tag_id", "baidu_tag_id"), props.getBaiduTagId()); } private static String resolvePlatform(Map 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 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 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 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); } }