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 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 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 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 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 impressionTracking = adxClient.reportImpression( bidResponse.getBid().getShowUrls(), bidResponse.getBid().getPrice()); recordTracking(record.getQk(), record.getTagId(), "impression", impressionTracking); } } List 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 normalizeParams(HttpServletRequest request) { Map 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 buildBidRequest(HttpServletRequest request, String traceId, Map 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 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 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 uid = baiduUidFromParams(params); if (!uid.isEmpty()) device.put("uid", uid); Map bidReq = new LinkedHashMap<>(); bidReq.put("mediaId", placement.getBaiduMediaId()); bidReq.put("reqId", baiduReqId(traceId)); bidReq.put("imp", List.of(imp)); bidReq.put("device", device); List 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 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 baiduUidFromParams(Map p) { Map 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> caid = baiduCaidFromParams(p); if (!caid.isEmpty()) { uid.put("caid", caid); } return uid; } private List> baiduCaidFromParams(Map 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> result = new ArrayList<>(); for (Object item : list) { Map caid = toCaidItem(item); if (!caid.isEmpty()) result.add(caid); } return result; } Map single = toCaidItem(value); return single.isEmpty() ? Collections.emptyList() : List.of(single); } catch (Exception e) { return List.of(Map.of("id", decoded)); } } private Map 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 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 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 params) { return TraceIds.normalize(firstParam(params, "callback", "request_id", "idfaMD5", "idfa", "oaidMD5", "oaid", "imeiMD5", "imei")); } private void normalizeDeductionParams(Map 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> 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 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 ignored) { } } return result; } private void alias(Map 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 uid, String key, String value) { if (value != null && !value.isBlank()) uid.put(key, value); } private void putUidWithMd5Fallback(Map 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 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 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; } }