package com.adx.tencent.honor; import com.adx.tencent.baidu.AdxClient; 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 jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.Semaphore; @RestController public class HonorTrackingController { private static final Logger log = LoggerFactory.getLogger(HonorTrackingController.class); private static final int MAX_CONCURRENT = 50; private final Semaphore impressionSemaphore = new Semaphore(MAX_CONCURRENT); private final Semaphore clickSemaphore = new Semaphore(MAX_CONCURRENT); private final AdxClient adxClient; private final HonorHotStore hotStore; private final HonorPlacement placement; public HonorTrackingController(AdxClient adxClient, HonorHotStore hotStore, @Nullable HonorPlacement placement) { this.adxClient = adxClient; this.hotStore = hotStore; this.placement = placement; } @GetMapping("/honor/impression") public Object impression(HttpServletRequest request) throws Exception { impressionSemaphore.acquire(); try { return doImpression(request); } finally { impressionSemaphore.release(); } } @GetMapping("/honor/click") public Object click(HttpServletRequest request, HttpServletResponse response) throws Exception { clickSemaphore.acquire(); try { return doClick(request, response); } finally { clickSemaphore.release(); } } private Object doImpression(HttpServletRequest request) throws Exception { if (placement == null) throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "honor placement not configured"); Map params = normalizeParams(request); String tagId = requireTagId(params); String traceId = honorTraceId(params); if (traceId == null || traceId.isBlank()) { log.warn("[荣耀][曝光] 缺少 traceId, params={}", params.keySet()); throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "missing honor trace id (trackId)"); } log.info("[荣耀][曝光] 收到曝光请求 | traceId={} | IP={} | 全部入参: {}", traceId, clientIp(request), params); NormalizedBidResponse response = adxClient.requestBid(buildBidRequest(request, traceId, params)); if (response == null) { log.warn("[荣耀][曝光] 百度ADX返回空响应, traceId={}", traceId); throw new ResponseStatusException(HttpStatus.BAD_GATEWAY, "empty baidu ADX response"); } log.info("[荣耀][曝光] 百度ADX返回: qk={}, hasBid={}, showUrls={}, clickUrls={}", response.getQk(), response.getBid() != null, response.getBid() != null && response.getBid().getShowUrls() != null ? response.getBid().getShowUrls().size() : 0, response.getBid() != null && response.getBid().getClickUrls() != null ? response.getBid().getClickUrls().size() : 0); HonorBidRecord record = bidRecordFromResponse(response, traceId, "impression", params); hotStore.recordBid(record); log.info("[荣耀][曝光] 已缓存bid, qk={}, traceId={}", record.getQk(), traceId); List tracking = Collections.emptyList(); if (response.getBid() != null && response.getBid().getShowUrls() != null && !response.getBid().getShowUrls().isEmpty()) { tracking = adxClient.reportImpression(response.getBid().getShowUrls(), response.getBid().getPrice()); log.info("[荣耀][曝光] 曝光上报完成, qk={}, results={}", record.getQk(), tracking); recordTracking(record.getQk(), tagId, "impression", tracking); } return Map.of("qk", record.getQk(), "traceId", traceId, "tagId", tagId); } private Object doClick(HttpServletRequest request, HttpServletResponse response) throws Exception { if (placement == null) throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "honor placement not configured"); Map params = normalizeParams(request); String tagId = requireTagId(params); String traceId = honorTraceId(params); if (traceId == null || traceId.isBlank()) { log.warn("[荣耀][点击] 缺少 traceId, params={}", params.keySet()); throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "missing honor trace id (trackId)"); } log.info("[荣耀][点击] 收到点击请求 | traceId={} | IP={} | 全部入参: {}", traceId, clientIp(request), params); HonorBidRecord record = hotStore.findBidByMediaTrace(traceId); if (record == null) { log.info("[荣耀][点击] Redis未命中, 发起竞价请求, traceId={}", traceId); NormalizedBidResponse bidResponse = adxClient.requestBid(buildBidRequest(request, traceId, params)); if (bidResponse == null) { log.warn("[荣耀][点击] 百度ADX返回空响应, traceId={}", traceId); throw new ResponseStatusException(HttpStatus.BAD_GATEWAY, "empty baidu ADX response"); } record = bidRecordFromResponse(bidResponse, traceId, "click", params); hotStore.recordBid(record); log.info("[荣耀][点击] 竞价成功, qk={}, traceId={}", record.getQk(), traceId); if (bidResponse.getBid() != null && bidResponse.getBid().getShowUrls() != null && !bidResponse.getBid().getShowUrls().isEmpty()) { List impressionTracking = adxClient.reportImpression( bidResponse.getBid().getShowUrls(), bidResponse.getBid().getPrice()); log.info("[荣耀][点击] Redis未命中补竞价后,曝光上报完成, qk={}, results={}", record.getQk(), impressionTracking); recordTracking(record.getQk(), tagId, "impression", impressionTracking); } } else { log.info("[荣耀][点击] Redis命中, qk={}, traceId={}", record.getQk(), traceId); } List tracking = adxClient.reportClick( record.getClickUrls() != null ? record.getClickUrls() : Collections.emptyList()); log.info("[荣耀][点击] 点击上报完成, qk={}, results={}", record.getQk(), tracking); recordTracking(record.getQk(), tagId, "click", tracking); try { hotStore.shrinkAfterClick(record); } catch (Exception e) { log.warn("[荣耀][点击] Redis缩减失败(不影响主流程), qk={}, error={}", record.getQk(), e.getMessage()); } String destination = record.getAppStoreLink() != null && !record.getAppStoreLink().isBlank() ? record.getAppStoreLink() : record.getLandingPage(); if (destination != null && !destination.isBlank()) { log.info("[荣耀][点击] 302重定向, qk={}, destination={}", record.getQk(), destination); response.sendRedirect(destination); return null; } return Map.of("qk", record.getQk(), "traceId", traceId, "tagId", tagId); } private Map buildBidRequest(HttpServletRequest request, String traceId, Map params) { HonorPlacement.Resolved resolved = placement.resolve(params); params.put("baidu_app_id", resolved.appId()); params.put("baidu_tag_id", resolved.tagId()); params.put("tagId", resolved.tagId()); if (resolved.platform() != null && !resolved.platform().isBlank()) params.put("baidu_platform", resolved.platform()); Map imp = new LinkedHashMap<>(); imp.put("id", "1"); imp.put("appId", resolved.appId()); imp.put("tagId", resolved.tagId()); imp.put("secure", 1); imp.put("adType", 0); imp.put("assets", List.of( Map.of("templateId", 1, "ratio", 2), Map.of("templateId", 1, "ratio", 7), Map.of("templateId", 2, "ratio", 1), Map.of("templateId", 2, "ratio", 4), Map.of("templateId", 3, "ratio", 2), Map.of("templateId", 4, "ratio", 4), Map.of("templateId", 4, "ratio", 5) )); imp.put("actionType", placement.getActionTypes() == null || placement.getActionTypes().isEmpty() ? List.of(0, 1, 2) : placement.getActionTypes()); imp.put("maxCount", 1); if (placement.getBidFloor() > 0) imp.put("bidFloor", placement.getBidFloor()); Map device = new LinkedHashMap<>(); device.put("ip", firstParam(params, "ip", "m6a") != null ? firstParam(params, "ip", "m6a") : clientIp(request)); device.put("ua", firstParam(params, "ua", "user_agent", "useragent") != null ? firstParam(params, "ua", "user_agent", "useragent") : defaultUa(resolved.platform())); device.put("deviceType", 1); if ("ios".equals(resolved.platform())) device.put("os", 1); if ("android".equals(resolved.platform())) device.put("os", 2); Map uid = new LinkedHashMap<>(); putUid(uid, "oaid", params.get("oaid")); putUid(uid, "oaidMd5", firstParam(params, "oaid_md5", "oaidMd5", "hash_oaid")); String muid = firstParam(params, "oaid_md5", "oaidMd5", "hash_oaid", "oaid"); putUid(uid, "muid", muid); if (!uid.isEmpty()) device.put("uid", uid); Map bidReq = new LinkedHashMap<>(); bidReq.put("mediaId", placement.getBaiduMediaId()); bidReq.put("reqId", traceId + "-" + Instant.now().toEpochMilli()); bidReq.put("imp", List.of(imp)); bidReq.put("device", device); List appList = parseAppList(firstParam(params, "appList", "app_list", "applist")); if (!appList.isEmpty()) bidReq.put("appList", appList); log.info("[荣耀][百度ADX] 竞价请求 | traceId={} | body={}", traceId, bidReq); return bidReq; } private HonorBidRecord bidRecordFromResponse(NormalizedBidResponse response, String traceId, String eventType, Map params) { HonorBidRecord record = new HonorBidRecord(); record.setQk(response.getQk()); record.setReqId(response.getReqId()); record.setBidId(response.getBidId()); record.setMedia("honor"); record.setMediaTraceId(traceId); record.setPlatform(params.get("baidu_platform")); record.setAdId(params.get("creativeId")); record.setAccountId(params.get("advertiserId")); record.setTagId(params.get("tagId")); record.setEventType(eventType); record.setMediaParams(params); record.setCreatedAt(Instant.now()); if (response.getBid() != null) { record.setCreativeId(response.getBid().getCrid()); record.setImpId(response.getBid().getImpId()); record.setTagId(response.getBid().getTagId() != null ? response.getBid().getTagId() : record.getTagId()); record.setPrice(response.getBid().getPrice()); record.setShowUrls(response.getBid().getShowUrls()); record.setClickUrls(response.getBid().getClickUrls()); record.setLandingPage(response.getBid().getLandingPage()); record.setAppStoreLink(response.getBid().getAppStoreLink()); if (response.getBid().getAdm() != null) { record.setPackageName(response.getBid().getAdm().getPackageName()); } } return record; } private void recordTracking(String qk, String tagId, String kind, List results) { if (results == null) return; for (TrackingResult result : results) { HonorTrackingRecord record = new HonorTrackingRecord(); record.setQk(qk); record.setTagId(tagId); record.setKind(kind); record.setUrl(result.getUrl()); record.setStatus(result.getStatus()); record.setOk(result.isOk()); record.setCreatedAt(Instant.now()); hotStore.recordTracking(record); } } private Map normalizeParams(HttpServletRequest request) { Map params = new HashMap<>(); request.getParameterMap().forEach((k, v) -> { if (v != null && v.length > 0 && v[0] != null) params.put(k, v[0]); }); copyAlias(params, "tagId", "tagId", "tag_id", "baidu_tag_id"); copyAlias(params, "trackId", "trackId", "track_id"); copyAlias(params, "advertiserId", "advertiserId", "advertiser_id"); copyAlias(params, "requestId", "requestId", "request_id", "REQUESTID"); copyAlias(params, "eventTime", "eventTime", "time"); copyAlias(params, "pkgName", "pkgName", "pkg_name", "packageName"); String os = firstParam(params, "platform", "os"); if (os != null) { String platform = HonorPlacement.normalizePlatform(os); if (platform != null) params.put("platform", platform); } return params; } private String requireTagId(Map params) { String tagId = params.get("tagId"); if (tagId == null || tagId.isBlank()) { throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "missing tagId"); } return tagId; } private String honorTraceId(Map params) { return firstParam(params, "trackId"); } private static void copyAlias(Map params, String target, String... aliases) { String value = firstParam(params, aliases); if (value != null) params.put(target, value); } private static String firstParam(Map p, String... keys) { for (String key : keys) { String value = p.get(key); if (value != null && !value.isBlank()) return value.trim(); } return null; } private List parseAppList(String value) { if (value == null || value.isBlank()) return Collections.emptyList(); List result = new ArrayList<>(); for (String part : value.split("[,|]")) { String item = part == null ? null : part.trim(); if (item == null || item.isEmpty()) continue; try { result.add(Integer.parseInt(item)); } catch (NumberFormatException e) { log.warn("[荣耀][百度ADX] 忽略非法 appList 值 | raw={} | item={}", value, item); } } return result; } private static void putUid(Map uid, String key, String value) { if (value != null && !value.isBlank()) uid.put(key, value); } private static String defaultUa(String platform) { 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 clientIp(HttpServletRequest request) { String xff = request.getHeader("X-Forwarded-For"); if (xff != null && !xff.isBlank()) { for (String part : xff.split(",")) { String ip = part.trim(); if (!ip.isEmpty()) return ip; } } String xri = request.getHeader("X-Real-IP"); if (xri != null && !xri.isBlank()) return xri.trim(); return request.getRemoteAddr(); } }