|
|
@@ -0,0 +1,538 @@
|
|
|
+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.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<>();
|
|
|
+ device.put("ip", firstParam(params, "ip") != null ? firstParam(params, "ip") : 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", 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("[快手][百度请求] 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 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 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;
|
|
|
+ }
|
|
|
+}
|