| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335 |
- 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<String, String> 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<TrackingResult> 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<String, String> 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<TrackingResult> 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<TrackingResult> 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<String, Object> buildBidRequest(HttpServletRequest request, String traceId, Map<String, String> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<Integer> 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<String, String> 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<TrackingResult> 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<String, String> normalizeParams(HttpServletRequest request) {
- Map<String, String> 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<String, String> 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<String, String> params) {
- return firstParam(params, "trackId");
- }
- private static void copyAlias(Map<String, String> params, String target, String... aliases) {
- String value = firstParam(params, aliases);
- if (value != null) params.put(target, value);
- }
- private static String firstParam(Map<String, String> p, String... keys) {
- for (String key : keys) {
- String value = p.get(key);
- if (value != null && !value.isBlank()) return value.trim();
- }
- return null;
- }
- private List<Integer> parseAppList(String value) {
- if (value == null || value.isBlank()) return Collections.emptyList();
- List<Integer> 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<String, Object> 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();
- }
- }
|