| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590 |
- package com.adx.tencent.kuaishou.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.kuaishou.model.KuaishouBidRecord;
- import com.adx.tencent.kuaishou.model.KuaishouTrackingRecord;
- import com.adx.tencent.kuaishou.service.KuaishouPlacement;
- import com.adx.tencent.kuaishou.store.KuaishouHotStore;
- import com.adx.tencent.util.TraceIds;
- import com.fasterxml.jackson.databind.ObjectMapper;
- 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.net.URLDecoder;
- import java.nio.charset.StandardCharsets;
- import java.security.MessageDigest;
- import java.time.Instant;
- import java.util.ArrayList;
- import java.util.Collections;
- import java.util.LinkedHashMap;
- import java.util.List;
- import java.util.Map;
- import java.util.concurrent.Semaphore;
- @RestController
- public class KuaishouTrackingController {
- private static final Logger log = LoggerFactory.getLogger(KuaishouTrackingController.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 KuaishouHotStore hotStore;
- private final KuaishouPlacement placement;
- private final ObjectMapper objectMapper;
- public KuaishouTrackingController(AdxClient adxClient,
- @Nullable KuaishouHotStore hotStore,
- @Nullable KuaishouPlacement placement,
- ObjectMapper objectMapper) {
- this.adxClient = adxClient;
- this.hotStore = hotStore;
- this.placement = placement;
- this.objectMapper = objectMapper;
- }
- @GetMapping("/kuaishou/impression")
- public Object impression(HttpServletRequest request) throws Exception {
- impressionSemaphore.acquire();
- try {
- return doImpression(request);
- } finally {
- impressionSemaphore.release();
- }
- }
- @GetMapping("/kuaishou/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 {
- ensureReady();
- Map<String, String> params = normalizeParams(request);
- String traceId = kuaishouTraceId(params);
- if (traceId == null || traceId.isBlank()) {
- log.warn("[快手][曝光] 缺少 traceId, params={}", params.keySet());
- throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "missing kuaishou trace id (callback)");
- }
- log.info("[快手][曝光][入参] traceId={} ip={} params={}", traceId, clientIp(request), params);
- Map<String, Object> bidRequest = buildBidRequest(request, traceId, params);
- NormalizedBidResponse response = adxClient.requestBid("kuaishou", traceId, bidRequest);
- if (response == null) {
- log.warn("[快手][曝光][百度结果] traceId={} empty=true", traceId);
- throw new ResponseStatusException(HttpStatus.BAD_GATEWAY, "empty baidu ADX response");
- }
- log.info("[快手][曝光][竞价结果] traceId={} qk={} bidId={} success={} price={} clickUrlCount={} showUrlCount={}",
- traceId,
- response.getQk(),
- response.getBidId(),
- response.getBid() != null,
- response.getBid() != null ? response.getBid().getPrice() : 0,
- response.getBid() != null && response.getBid().getClickUrls() != null ? response.getBid().getClickUrls().size() : 0,
- response.getBid() != null && response.getBid().getShowUrls() != null ? response.getBid().getShowUrls().size() : 0);
- KuaishouBidRecord record = bidRecordFromResponse(response, traceId, "impression", params);
- hotStore.recordBid(record);
- if (response.getBid() != null && response.getBid().getShowUrls() != null && !response.getBid().getShowUrls().isEmpty()) {
- List<TrackingResult> tracking = adxClient.reportImpression(response.getBid().getShowUrls(), response.getBid().getPrice());
- recordTracking(record.getQk(), record.getTagId(), "impression", tracking);
- }
- return Map.of("qk", record.getQk(), "traceId", traceId, "tagId", record.getTagId());
- }
- private Object doClick(HttpServletRequest request, HttpServletResponse response) throws Exception {
- ensureReady();
- Map<String, String> params = normalizeParams(request);
- String traceId = kuaishouTraceId(params);
- if (traceId == null || traceId.isBlank()) {
- log.warn("[快手][点击] 缺少 traceId, params={}", params.keySet());
- throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "missing kuaishou trace id (callback)");
- }
- log.info("[快手][点击][入参] traceId={} ip={} params={}", traceId, clientIp(request), params);
- KuaishouBidRecord record = hotStore.findBidByMediaTrace(traceId);
- if (record == null) {
- NormalizedBidResponse bidResponse = adxClient.requestBid("kuaishou", traceId, buildBidRequest(request, traceId, params));
- if (bidResponse == null) {
- log.warn("[快手][点击][百度结果] traceId={} empty=true", traceId);
- throw new ResponseStatusException(HttpStatus.BAD_GATEWAY, "empty baidu ADX response");
- }
- log.info("[快手][点击][竞价结果] traceId={} qk={} bidId={} success={} price={} clickUrlCount={} showUrlCount={}",
- traceId,
- bidResponse.getQk(),
- bidResponse.getBidId(),
- bidResponse.getBid() != null,
- bidResponse.getBid() != null ? bidResponse.getBid().getPrice() : 0,
- bidResponse.getBid() != null && bidResponse.getBid().getClickUrls() != null ? bidResponse.getBid().getClickUrls().size() : 0,
- bidResponse.getBid() != null && bidResponse.getBid().getShowUrls() != null ? bidResponse.getBid().getShowUrls().size() : 0);
- record = bidRecordFromResponse(bidResponse, traceId, "click", params);
- hotStore.recordBid(record);
- if (bidResponse.getBid() != null && bidResponse.getBid().getShowUrls() != null && !bidResponse.getBid().getShowUrls().isEmpty()) {
- List<TrackingResult> impressionTracking = adxClient.reportImpression(
- bidResponse.getBid().getShowUrls(), bidResponse.getBid().getPrice());
- recordTracking(record.getQk(), record.getTagId(), "impression", impressionTracking);
- }
- }
- List<TrackingResult> tracking = adxClient.reportClick(
- record.getClickUrls() != null ? record.getClickUrls() : Collections.emptyList());
- recordTracking(record.getQk(), record.getTagId(), "click", tracking);
- try {
- hotStore.shrinkAfterClick(record);
- } catch (Exception ignored) {
- }
- String destination = record.getAppStoreLink() != null && !record.getAppStoreLink().isBlank()
- ? record.getAppStoreLink() : record.getLandingPage();
- if (destination != null && !destination.isBlank()) {
- response.sendRedirect(destination);
- return null;
- }
- return Map.of("qk", record.getQk(), "traceId", traceId, "tagId", record.getTagId());
- }
- private void ensureReady() {
- if (placement == null || hotStore == null) {
- throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "kuaishou placement not configured");
- }
- }
- private Map<String, String> normalizeParams(HttpServletRequest request) {
- Map<String, String> params = new LinkedHashMap<>();
- request.getParameterMap().forEach((k, v) -> {
- if (v != null && v.length > 0 && v[0] != null) {
- params.put(k, v[0].trim());
- }
- });
- alias(params, "tagId", "tag_id", "baidu_tag_id");
- alias(params, "callback", "cb");
- alias(params, "platform", "os", "system", "device_os");
- alias(params, "accountId", "accountid");
- alias(params, "accountid", "accountId");
- alias(params, "ua", "user_agent", "useragent");
- alias(params, "ip", "ipv4", "m6a");
- alias(params, "oaidMD5", "oaid_md5", "oaidMd5", "oaidMd5");
- alias(params, "imeiMD5", "imei_md5", "imeiMd5");
- alias(params, "imeiSHA1", "imei_sha1", "imeiSha1");
- alias(params, "androidIdMD5", "android_id_md5", "androidIdMd5");
- alias(params, "androidIdSHA1", "android_id_sha1", "androidIdSha1");
- alias(params, "idfaMD5", "idfa_md5", "idfaMd5");
- alias(params, "idfaSHA1", "idfa_sha1", "idfaSha1");
- alias(params, "kenyid_caa", "caid", "kenyIdCaa");
- alias(params, "ts", "eventTime", "timestamp");
- normalizeDeductionParams(params);
- return params;
- }
- private Map<String, Object> buildBidRequest(HttpServletRequest request, String traceId, Map<String, String> params) {
- KuaishouPlacement.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());
- params.put("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", defaultBaiduAssets());
- 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<>();
- putDeviceIp(device, firstParam(params, "ip"), firstParam(params, "ipv6"), clientIp(request));
- device.put("ua", normalizeUserAgent(firstParam(params, "ua"), resolved.platform()));
- device.put("deviceType", 1);
- int os = baiduOsForPlatform(resolved.platform());
- if (os != 0) device.put("os", os);
- Map<String, Object> uid = baiduUidFromParams(params);
- if (!uid.isEmpty()) device.put("uid", uid);
- Map<String, Object> bidReq = new LinkedHashMap<>();
- bidReq.put("mediaId", placement.getBaiduMediaId());
- bidReq.put("reqId", baiduReqId(traceId));
- bidReq.put("imp", List.of(imp));
- bidReq.put("device", device);
- List<Integer> appList = parseAppList(firstParam(params, "appList", "app_list", "applist"));
- bidReq.put("appList", appList);
- log.info("[快手][百度请求] traceId={} body={}", traceId, toJson(bidReq));
- return bidReq;
- }
- private KuaishouBidRecord bidRecordFromResponse(NormalizedBidResponse response,
- String traceId,
- String eventType,
- Map<String, String> params) {
- KuaishouBidRecord record = new KuaishouBidRecord();
- record.setQk(response.getQk());
- record.setReqId(response.getReqId());
- record.setBidId(response.getBidId());
- record.setMedia("kuaishou");
- record.setMediaTraceId(traceId);
- record.setPlatform(params.get("baidu_platform"));
- record.setAccountId(firstParam(params, "accountId", "accountid"));
- 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 Map<String, Object> baiduUidFromParams(Map<String, String> p) {
- Map<String, Object> uid = new LinkedHashMap<>();
- putUidWithMd5Fallback(uid, "did", "didMd5",
- firstParam(p, "imei"),
- firstParam(p, "imeiMD5", "imei_md5", "imeiMd5"));
- putUidWithMd5Fallback(uid, "dpid", "dpidMd5",
- firstParam(p, "androidid", "androidId", "android_id"),
- firstParam(p, "androidIdMD5", "android_id_md5", "androidIdMd5"));
- putUidWithMd5Fallback(uid, "idfa", "idfaMd5",
- firstParam(p, "idfa"),
- firstParam(p, "idfaMD5", "idfa_md5", "idfaMd5"));
- putUidWithMd5Fallback(uid, "oaid", "oaidMd5",
- firstParam(p, "oaid"),
- firstParam(p, "oaidMD5", "oaid_md5", "oaidMd5"));
- List<Map<String, Object>> caid = baiduCaidFromParams(p);
- if (!caid.isEmpty()) {
- uid.put("caid", caid);
- }
- return uid;
- }
- private List<Map<String, Object>> baiduCaidFromParams(Map<String, String> params) {
- String rawCaid = firstParam(params, "kenyid_caa", "caid");
- if (rawCaid == null || rawCaid.isBlank()) {
- return Collections.emptyList();
- }
- String decoded;
- try {
- decoded = URLDecoder.decode(rawCaid, StandardCharsets.UTF_8);
- } catch (Exception e) {
- decoded = rawCaid;
- }
- try {
- Object value = objectMapper.readValue(decoded, Object.class);
- if (value instanceof List<?> list) {
- List<Map<String, Object>> result = new ArrayList<>();
- for (Object item : list) {
- Map<String, Object> caid = toCaidItem(item);
- if (!caid.isEmpty()) result.add(caid);
- }
- return result;
- }
- Map<String, Object> single = toCaidItem(value);
- return single.isEmpty() ? Collections.emptyList() : List.of(single);
- } catch (Exception e) {
- return List.of(Map.of("id", decoded));
- }
- }
- private Map<String, Object> toCaidItem(Object source) {
- if (!(source instanceof Map<?, ?> map)) {
- return Collections.emptyMap();
- }
- String id = firstNonBlankValue(map, "qaid", "id", "caid");
- if (id == null) {
- return Collections.emptyMap();
- }
- Map<String, Object> caid = new LinkedHashMap<>();
- caid.put("id", id);
- String version = firstNonBlankValue(map, "version");
- if (version != null) {
- caid.put("version", version);
- }
- Long generateTime = firstLongValue(map, "generateTime");
- if (generateTime != null) {
- caid.put("generateTime", generateTime);
- }
- Long vendor = firstLongValue(map, "vendor");
- if (vendor != null) {
- caid.put("vendor", vendor);
- }
- return caid;
- }
- private void recordTracking(String qk, String tagId, String kind, List<TrackingResult> results) {
- if (results == null) return;
- for (TrackingResult result : results) {
- KuaishouTrackingRecord record = new KuaishouTrackingRecord();
- 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 String kuaishouTraceId(Map<String, String> params) {
- return TraceIds.normalize(firstParam(params, "callback", "request_id", "idfaMD5", "idfa", "oaidMD5", "oaid", "imeiMD5", "imei"));
- }
- private void normalizeDeductionParams(Map<String, String> params) {
- String rate = firstParam(params, "deduction_rate", "deductionRate", "rate");
- int sanitized = 0;
- try {
- if (rate != null) sanitized = Math.max(0, Math.min(100, Integer.parseInt(rate)));
- } catch (NumberFormatException ignored) {
- }
- params.put(KuaishouBidRecord.DEDUCTION_RATE_KEY, String.valueOf(sanitized));
- }
- private List<Map<String, Integer>> defaultBaiduAssets() {
- return 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)
- );
- }
- private int baiduOsForPlatform(String platform) {
- if (platform == null) return 0;
- return switch (platform) {
- case "ios" -> 1;
- case "android" -> 2;
- default -> 0;
- };
- }
- private static String normalizeUserAgent(String ua, String platform) {
- if (ua != null && !ua.isBlank()) return ua;
- 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 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 ignored) {
- }
- }
- return result;
- }
- private void alias(Map<String, String> params, String target, String... aliases) {
- if (params.get(target) != null && !params.get(target).isBlank()) return;
- for (String alias : aliases) {
- String value = params.get(alias);
- if (value != null && !value.isBlank()) {
- params.put(target, value.trim());
- return;
- }
- }
- }
- private void putUid(Map<String, Object> uid, String key, String value) {
- if (value != null && !value.isBlank()) uid.put(key, value);
- }
- private void putUidWithMd5Fallback(Map<String, Object> uid,
- String rawKey,
- String md5Key,
- String rawCandidate,
- String explicitMd5Candidate) {
- String md5Value = normalizeMd5(explicitMd5Candidate);
- if (md5Value != null) {
- uid.put(md5Key, md5Value);
- }
- if (rawCandidate == null || rawCandidate.isBlank()) {
- return;
- }
- String value = rawCandidate.trim();
- if (isMd5(value)) {
- uid.putIfAbsent(md5Key, value.toLowerCase());
- return;
- }
- uid.put(rawKey, value);
- uid.putIfAbsent(md5Key, md5Hex(value));
- }
- private static boolean isMd5(String value) {
- return value != null && value.matches("(?i)^[0-9a-f]{32}$");
- }
- private static String normalizeMd5(String value) {
- if (value == null || value.isBlank()) {
- return null;
- }
- String trimmed = value.trim();
- return isMd5(trimmed) ? trimmed.toLowerCase() : trimmed;
- }
- private static String md5Hex(String value) {
- try {
- MessageDigest md = MessageDigest.getInstance("MD5");
- byte[] digest = md.digest(value.getBytes(StandardCharsets.UTF_8));
- StringBuilder sb = new StringBuilder(digest.length * 2);
- for (byte b : digest) {
- sb.append(String.format("%02x", b));
- }
- return sb.toString();
- } catch (Exception e) {
- return value;
- }
- }
- private String baiduReqId(String traceId) {
- String base = traceId == null ? "" : traceId.replaceAll("[^A-Za-z0-9_-]", "");
- if (base.isBlank()) {
- base = md5Hex(traceId == null ? "kuaishou" : traceId);
- }
- if (base.length() > 64) {
- base = base.substring(0, 64);
- }
- return base + "-" + Instant.now().toEpochMilli();
- }
- private static void putDeviceIp(Map<String, Object> device, String ipv4Candidate, String ipv6Candidate, String fallbackIp) {
- String ipv4 = normalizeIp(ipv4Candidate);
- String ipv6 = normalizeIp(ipv6Candidate);
- String fallback = normalizeIp(fallbackIp);
- if (ipv4 == null && ipv6 == null && fallback != null) {
- if (isIpv6(fallback)) {
- ipv6 = fallback;
- } else {
- ipv4 = fallback;
- }
- }
- if (ipv4 != null && isIpv6(ipv4)) {
- ipv6 = ipv4;
- ipv4 = null;
- }
- if (ipv6 != null && !isIpv6(ipv6)) {
- if (ipv4 == null) {
- ipv4 = ipv6;
- }
- ipv6 = null;
- }
- if (ipv4 != null) {
- device.put("ip", ipv4);
- }
- if (ipv6 != null) {
- device.put("ipv6", ipv6);
- }
- }
- private static String normalizeIp(String value) {
- if (value == null || value.isBlank()) {
- return null;
- }
- return value.trim();
- }
- private static boolean isIpv6(String value) {
- return value != null && value.contains(":");
- }
- private String firstParam(Map<String, String> params, String... keys) {
- for (String key : keys) {
- String value = params.get(key);
- if (value != null && !value.isBlank()) return value.trim();
- }
- return null;
- }
- private static String firstNonBlankValue(Map<?, ?> map, String... keys) {
- for (String key : keys) {
- Object value = map.get(key);
- if (value == null) continue;
- String text = String.valueOf(value).trim();
- if (!text.isEmpty()) return text;
- }
- return null;
- }
- private static Long firstLongValue(Map<?, ?> map, String... keys) {
- for (String key : keys) {
- Object value = map.get(key);
- if (value == null) continue;
- if (value instanceof Number number) return number.longValue();
- try {
- return Long.parseLong(String.valueOf(value).trim());
- } catch (NumberFormatException ignored) {
- }
- }
- return null;
- }
- private String toJson(Object value) {
- try {
- return objectMapper.writeValueAsString(value);
- } catch (Exception e) {
- return String.valueOf(value);
- }
- }
- 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();
- }
- private static String safe(String value) {
- return value == null ? "" : value;
- }
- }
|