|
@@ -15,6 +15,7 @@ import java.net.URI;
|
|
|
import java.net.http.HttpClient;
|
|
import java.net.http.HttpClient;
|
|
|
import java.net.http.HttpRequest;
|
|
import java.net.http.HttpRequest;
|
|
|
import java.net.http.HttpResponse;
|
|
import java.net.http.HttpResponse;
|
|
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
import java.security.MessageDigest;
|
|
import java.security.MessageDigest;
|
|
|
import java.time.Duration;
|
|
import java.time.Duration;
|
|
|
import java.time.Instant;
|
|
import java.time.Instant;
|
|
@@ -22,37 +23,28 @@ import java.util.*;
|
|
|
import java.util.function.Supplier;
|
|
import java.util.function.Supplier;
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 腾讯行为回传客户端。
|
|
|
|
|
- * 调用腾讯 Marketing API user_actions/add 接口回传转化数据。
|
|
|
|
|
- *
|
|
|
|
|
- * API文档: https://developers.e.qq.com/v3.0/docs/api/user_actions/add
|
|
|
|
|
|
|
+ * 腾讯转化回传客户端。
|
|
|
|
|
+ * 通过腾讯 callback URL (tracking.e.qq.com/conv 或 api.e.qq.com/conv) POST 回传转化数据。
|
|
|
|
|
+ * callback URL 来自监测链接下发的 __CALLBACK__ 宏,存储在 Redis BidRecord.mediaParams.callback。
|
|
|
*/
|
|
*/
|
|
|
public class TencentClient {
|
|
public class TencentClient {
|
|
|
|
|
|
|
|
private static final Logger log = LoggerFactory.getLogger(TencentClient.class);
|
|
private static final Logger log = LoggerFactory.getLogger(TencentClient.class);
|
|
|
|
|
|
|
|
- private final String apiBaseUrl;
|
|
|
|
|
private final Supplier<String> accessTokenSupplier;
|
|
private final Supplier<String> accessTokenSupplier;
|
|
|
- private final long androidUserActionSetId;
|
|
|
|
|
- private final long iosUserActionSetId;
|
|
|
|
|
private final Map<Integer, String> actMap; // baiduAct -> tencentActionType
|
|
private final Map<Integer, String> actMap; // baiduAct -> tencentActionType
|
|
|
private final HttpClient httpClient;
|
|
private final HttpClient httpClient;
|
|
|
private final ObjectMapper objectMapper;
|
|
private final ObjectMapper objectMapper;
|
|
|
|
|
|
|
|
- public TencentClient(String apiBaseUrl, Supplier<String> accessTokenSupplier,
|
|
|
|
|
- long androidUserActionSetId, long iosUserActionSetId,
|
|
|
|
|
|
|
+ public TencentClient(Supplier<String> accessTokenSupplier,
|
|
|
Map<Integer, String> actMapOverride,
|
|
Map<Integer, String> actMapOverride,
|
|
|
ObjectMapper objectMapper) {
|
|
ObjectMapper objectMapper) {
|
|
|
- this.apiBaseUrl = apiBaseUrl.replaceAll("/+$", "");
|
|
|
|
|
this.accessTokenSupplier = accessTokenSupplier;
|
|
this.accessTokenSupplier = accessTokenSupplier;
|
|
|
- this.androidUserActionSetId = androidUserActionSetId;
|
|
|
|
|
- this.iosUserActionSetId = iosUserActionSetId;
|
|
|
|
|
this.objectMapper = objectMapper;
|
|
this.objectMapper = objectMapper;
|
|
|
this.httpClient = HttpClient.newBuilder()
|
|
this.httpClient = HttpClient.newBuilder()
|
|
|
.connectTimeout(Duration.ofSeconds(10))
|
|
.connectTimeout(Duration.ofSeconds(10))
|
|
|
.build();
|
|
.build();
|
|
|
|
|
|
|
|
- // 从默认映射表开始,再用配置覆盖
|
|
|
|
|
Map<Integer, String> merged = new HashMap<>(defaultActMap());
|
|
Map<Integer, String> merged = new HashMap<>(defaultActMap());
|
|
|
if (actMapOverride != null) merged.putAll(actMapOverride);
|
|
if (actMapOverride != null) merged.putAll(actMapOverride);
|
|
|
this.actMap = Collections.unmodifiableMap(merged);
|
|
this.actMap = Collections.unmodifiableMap(merged);
|
|
@@ -70,23 +62,12 @@ public class TencentClient {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 根据平台获取对应的 user_action_set_id。
|
|
|
|
|
- */
|
|
|
|
|
- public long getUserActionSetId(String platform) {
|
|
|
|
|
- if ("ios".equalsIgnoreCase(platform)) {
|
|
|
|
|
- return iosUserActionSetId;
|
|
|
|
|
- }
|
|
|
|
|
- return androidUserActionSetId;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * 发送转化事件到腾讯。
|
|
|
|
|
- * 构造 user_actions/add 请求体并发送 POST 请求。
|
|
|
|
|
|
|
+ * 通过 callback URL 回传转化事件到腾讯。
|
|
|
*
|
|
*
|
|
|
- * @param bid 匹配到的竞价记录
|
|
|
|
|
|
|
+ * @param bid 匹配到的竞价记录(含 callback URL)
|
|
|
* @param payment 百度转化支付信息
|
|
* @param payment 百度转化支付信息
|
|
|
* @param platform 平台标识("android" 或 "ios")
|
|
* @param platform 平台标识("android" 或 "ios")
|
|
|
- * @return MediaCallbackRecord 记录回传结果(包含成功/失败信息)
|
|
|
|
|
|
|
+ * @return MediaCallbackRecord 记录回传结果(含请求体、响应、状态)
|
|
|
*/
|
|
*/
|
|
|
public MediaCallbackRecord sendConversionEvent(BidRecord bid, PaymentInfo payment, String platform) {
|
|
public MediaCallbackRecord sendConversionEvent(BidRecord bid, PaymentInfo payment, String platform) {
|
|
|
long eventTimeMs = Instant.now().toEpochMilli();
|
|
long eventTimeMs = Instant.now().toEpochMilli();
|
|
@@ -101,92 +82,44 @@ public class TencentClient {
|
|
|
record.setAttempt(1);
|
|
record.setAttempt(1);
|
|
|
record.setCreatedAt(Instant.now());
|
|
record.setCreatedAt(Instant.now());
|
|
|
|
|
|
|
|
- // 构造请求 URL
|
|
|
|
|
- long timestamp = Instant.now().getEpochSecond();
|
|
|
|
|
- String nonce = UUID.randomUUID().toString().replace("-", "").substring(0, 32);
|
|
|
|
|
- String currentToken = accessTokenSupplier.get();
|
|
|
|
|
- if (currentToken == null || currentToken.isBlank()) {
|
|
|
|
|
|
|
+ // 取 callback URL
|
|
|
|
|
+ String callbackUrl = bid.getMediaParams() != null ? bid.getMediaParams().get("callback") : null;
|
|
|
|
|
+ if (callbackUrl == null || callbackUrl.isBlank()) {
|
|
|
record.setOk(false);
|
|
record.setOk(false);
|
|
|
- record.setErrorMessage("access_token is empty (check Redis key)");
|
|
|
|
|
- log.warn("[Tencent] access_token 为空, qk={}", payment.getQk());
|
|
|
|
|
|
|
+ record.setErrorMessage("callback URL 缺失, 无法回传");
|
|
|
|
|
+ log.warn("[Tencent] callback URL 缺失, qk={}", payment.getQk());
|
|
|
return record;
|
|
return record;
|
|
|
}
|
|
}
|
|
|
- String url = String.format("%s/user_actions/add?access_token=%s×tamp=%d&nonce=%s",
|
|
|
|
|
- apiBaseUrl, currentToken, timestamp, nonce);
|
|
|
|
|
- record.setCallbackUrl(url);
|
|
|
|
|
|
|
+ record.setCallbackUrl(callbackUrl);
|
|
|
|
|
|
|
|
- // 构造请求 Body
|
|
|
|
|
|
|
+ // 构造 actions body
|
|
|
UserAction action = buildUserAction(bid, payment, actionType, eventTimeMs);
|
|
UserAction action = buildUserAction(bid, payment, actionType, eventTimeMs);
|
|
|
-
|
|
|
|
|
- long actionSetId = getUserActionSetId(platform);
|
|
|
|
|
- // account_id: 优先顶层字段,兜底从 mediaParams 读取(兼容旧数据)
|
|
|
|
|
- long finalAccountId = 0;
|
|
|
|
|
- String accountIdStr = bid.getAccountId();
|
|
|
|
|
- if (accountIdStr == null || accountIdStr.isBlank()) {
|
|
|
|
|
- accountIdStr = bid.getMediaParams() != null ? bid.getMediaParams().get("account_id") : null;
|
|
|
|
|
- }
|
|
|
|
|
- if (accountIdStr != null && !accountIdStr.isBlank()) {
|
|
|
|
|
- try {
|
|
|
|
|
- finalAccountId = Long.parseLong(accountIdStr);
|
|
|
|
|
- } catch (NumberFormatException ignored) {}
|
|
|
|
|
- }
|
|
|
|
|
- if (finalAccountId == 0) {
|
|
|
|
|
- log.warn("[Tencent] account_id 缺失, qk={}, 监测链接未传入account_id", payment.getQk());
|
|
|
|
|
- }
|
|
|
|
|
Map<String, Object> body = new LinkedHashMap<>();
|
|
Map<String, Object> body = new LinkedHashMap<>();
|
|
|
- body.put("account_id", finalAccountId);
|
|
|
|
|
- body.put("user_action_set_id", actionSetId);
|
|
|
|
|
body.put("actions", List.of(action));
|
|
body.put("actions", List.of(action));
|
|
|
- // ad_id: 传入广告ID用于归因匹配
|
|
|
|
|
- if (bid.getAdId() != null && !bid.getAdId().isBlank()) {
|
|
|
|
|
- body.put("ad_id", bid.getAdId());
|
|
|
|
|
- }
|
|
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
byte[] payload = objectMapper.writeValueAsBytes(body);
|
|
byte[] payload = objectMapper.writeValueAsBytes(body);
|
|
|
- String payloadStr = new String(payload, java.nio.charset.StandardCharsets.UTF_8);
|
|
|
|
|
- log.info("[Tencent] sending user_action: qk={}, actionType={}, baiduAct={}, url={}, body={}",
|
|
|
|
|
- payment.getQk(), actionType, payment.getAct(), url, payloadStr);
|
|
|
|
|
|
|
+ String payloadStr = new String(payload, StandardCharsets.UTF_8);
|
|
|
|
|
+ record.setRequestBody(payloadStr);
|
|
|
|
|
|
|
|
- HttpRequest request = HttpRequest.newBuilder()
|
|
|
|
|
- .uri(URI.create(url))
|
|
|
|
|
- .POST(HttpRequest.BodyPublishers.ofByteArray(payload))
|
|
|
|
|
- .header("Content-Type", "application/json")
|
|
|
|
|
- .timeout(Duration.ofSeconds(15))
|
|
|
|
|
- .build();
|
|
|
|
|
|
|
+ log.info("[Tencent] callback上报: qk={}, actionType={}, baiduAct={}, url={}, body={}",
|
|
|
|
|
+ payment.getQk(), actionType, payment.getAct(), callbackUrl, payloadStr);
|
|
|
|
|
|
|
|
|
|
+ HttpRequest request = buildCallbackRequest(callbackUrl, payload);
|
|
|
HttpResponse<byte[]> response = httpClient.send(request, HttpResponse.BodyHandlers.ofByteArray());
|
|
HttpResponse<byte[]> response = httpClient.send(request, HttpResponse.BodyHandlers.ofByteArray());
|
|
|
- int status = response.statusCode();
|
|
|
|
|
- String responseBody = truncate(new String(response.body()), 4096);
|
|
|
|
|
- record.setStatus(status);
|
|
|
|
|
- record.setResponseBody(responseBody);
|
|
|
|
|
|
|
|
|
|
- // 解析响应判断是否成功
|
|
|
|
|
- if (status >= 200 && status < 300) {
|
|
|
|
|
- TencentResponse tencentResp = objectMapper.readValue(response.body(), TencentResponse.class);
|
|
|
|
|
- record.setOk(tencentResp.isSuccess());
|
|
|
|
|
- if (!tencentResp.isSuccess()) {
|
|
|
|
|
- record.setErrorMessage("tencent api error: code=" + tencentResp.getCode()
|
|
|
|
|
- + " msg=" + tencentResp.getMessage());
|
|
|
|
|
- }
|
|
|
|
|
- } else {
|
|
|
|
|
- record.setOk(false);
|
|
|
|
|
- record.setErrorMessage("HTTP status: " + status);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- log.info("[Tencent] callback result: qk={}, actionType={}, status={}, ok={}, response={}",
|
|
|
|
|
- payment.getQk(), actionType, status, record.isOk(), responseBody);
|
|
|
|
|
|
|
+ handleResponse(record, response, payment.getQk(), actionType);
|
|
|
} catch (Exception e) {
|
|
} catch (Exception e) {
|
|
|
record.setOk(false);
|
|
record.setOk(false);
|
|
|
record.setErrorMessage(e.getMessage());
|
|
record.setErrorMessage(e.getMessage());
|
|
|
- log.warn("[Tencent] callback failed: qk={}, actionType={}, error={}",
|
|
|
|
|
|
|
+ log.warn("[Tencent] callback失败: qk={}, actionType={}, error={}",
|
|
|
payment.getQk(), actionType, e.getMessage());
|
|
payment.getQk(), actionType, e.getMessage());
|
|
|
}
|
|
}
|
|
|
return record;
|
|
return record;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 重试已有回调记录。
|
|
|
|
|
|
|
+ * 重试已有回调记录,使用存储的 callbackUrl 和 requestBody。
|
|
|
*/
|
|
*/
|
|
|
public MediaCallbackRecord retryCallback(MediaCallbackRecord original)
|
|
public MediaCallbackRecord retryCallback(MediaCallbackRecord original)
|
|
|
throws IOException, InterruptedException {
|
|
throws IOException, InterruptedException {
|
|
@@ -194,51 +127,32 @@ public class TencentClient {
|
|
|
retry.setAttempt(original.getAttempt() + 1);
|
|
retry.setAttempt(original.getAttempt() + 1);
|
|
|
retry.setCreatedAt(Instant.now());
|
|
retry.setCreatedAt(Instant.now());
|
|
|
|
|
|
|
|
- try {
|
|
|
|
|
- log.info("[Tencent] retrying callback: dedupeKey={}, attempt={}",
|
|
|
|
|
- original.getDedupeKey(), retry.getAttempt());
|
|
|
|
|
|
|
+ String callbackUrl = original.getCallbackUrl();
|
|
|
|
|
+ String requestBody = original.getRequestBody();
|
|
|
|
|
|
|
|
- // 重新构造请求(使用新的 timestamp 和 nonce)
|
|
|
|
|
- long timestamp = Instant.now().getEpochSecond();
|
|
|
|
|
- String nonce = UUID.randomUUID().toString().replace("-", "").substring(0, 32);
|
|
|
|
|
- String currentToken = accessTokenSupplier.get();
|
|
|
|
|
- if (currentToken == null || currentToken.isBlank()) {
|
|
|
|
|
- retry.setOk(false);
|
|
|
|
|
- retry.setErrorMessage("access_token is empty (check Redis key)");
|
|
|
|
|
- return retry;
|
|
|
|
|
- }
|
|
|
|
|
- String url = String.format("%s/user_actions/add?access_token=%s×tamp=%d&nonce=%s",
|
|
|
|
|
- apiBaseUrl, currentToken, timestamp, nonce);
|
|
|
|
|
-
|
|
|
|
|
- // 需要从 original 解析出原始 body 重发
|
|
|
|
|
- // 简化处理:直接用原始的 callbackUrl 重新发送一个空 body 触发
|
|
|
|
|
- // 实际上重试需要存储原始 body,这里用 responseBody 字段暂存
|
|
|
|
|
- HttpRequest request = HttpRequest.newBuilder()
|
|
|
|
|
- .uri(URI.create(original.getCallbackUrl()))
|
|
|
|
|
- .POST(HttpRequest.BodyPublishers.noBody())
|
|
|
|
|
- .header("Content-Type", "application/json")
|
|
|
|
|
- .timeout(Duration.ofSeconds(15))
|
|
|
|
|
- .build();
|
|
|
|
|
|
|
+ if (callbackUrl == null || callbackUrl.isBlank()) {
|
|
|
|
|
+ retry.setOk(false);
|
|
|
|
|
+ retry.setErrorMessage("callback URL 缺失, 无法重试");
|
|
|
|
|
+ return retry;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (requestBody == null || requestBody.isBlank()) {
|
|
|
|
|
+ retry.setOk(false);
|
|
|
|
|
+ retry.setErrorMessage("requestBody 缺失, 无法重试");
|
|
|
|
|
+ return retry;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
|
|
+ try {
|
|
|
|
|
+ log.info("[Tencent] retrying callback: dedupeKey={}, attempt={}, url={}",
|
|
|
|
|
+ original.getDedupeKey(), retry.getAttempt(), callbackUrl);
|
|
|
|
|
+
|
|
|
|
|
+ byte[] payload = requestBody.getBytes(StandardCharsets.UTF_8);
|
|
|
|
|
+ HttpRequest request = buildCallbackRequest(callbackUrl, payload);
|
|
|
HttpResponse<byte[]> response = httpClient.send(request, HttpResponse.BodyHandlers.ofByteArray());
|
|
HttpResponse<byte[]> response = httpClient.send(request, HttpResponse.BodyHandlers.ofByteArray());
|
|
|
- int status = response.statusCode();
|
|
|
|
|
- retry.setStatus(status);
|
|
|
|
|
- retry.setResponseBody(truncate(new String(response.body()), 4096));
|
|
|
|
|
|
|
|
|
|
- if (status >= 200 && status < 300) {
|
|
|
|
|
- TencentResponse tencentResp = objectMapper.readValue(response.body(), TencentResponse.class);
|
|
|
|
|
- retry.setOk(tencentResp.isSuccess());
|
|
|
|
|
- if (!tencentResp.isSuccess()) {
|
|
|
|
|
- retry.setErrorMessage("tencent api error: code=" + tencentResp.getCode()
|
|
|
|
|
- + " msg=" + tencentResp.getMessage());
|
|
|
|
|
- }
|
|
|
|
|
- } else {
|
|
|
|
|
- retry.setOk(false);
|
|
|
|
|
- retry.setErrorMessage("HTTP status: " + status);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ handleResponse(retry, response, original.getQk(), "retry");
|
|
|
|
|
|
|
|
log.info("[Tencent] retry result: dedupeKey={}, status={}, ok={}",
|
|
log.info("[Tencent] retry result: dedupeKey={}, status={}, ok={}",
|
|
|
- original.getDedupeKey(), status, retry.isOk());
|
|
|
|
|
|
|
+ original.getDedupeKey(), retry.getStatus(), retry.isOk());
|
|
|
} catch (IOException | InterruptedException e) {
|
|
} catch (IOException | InterruptedException e) {
|
|
|
retry.setOk(false);
|
|
retry.setOk(false);
|
|
|
retry.setErrorMessage(e.getMessage());
|
|
retry.setErrorMessage(e.getMessage());
|
|
@@ -249,6 +163,63 @@ public class TencentClient {
|
|
|
return retry;
|
|
return retry;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // ─── 内部方法 ────────────────────────────────────────────────────────────
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 构造 callback POST 请求,自动判断是否需要鉴权 header。
|
|
|
|
|
+ */
|
|
|
|
|
+ private HttpRequest buildCallbackRequest(String callbackUrl, byte[] payload) {
|
|
|
|
|
+ boolean needAuth = callbackUrl.contains("api.e.qq.com");
|
|
|
|
|
+
|
|
|
|
|
+ HttpRequest.Builder reqBuilder = HttpRequest.newBuilder()
|
|
|
|
|
+ .uri(URI.create(callbackUrl))
|
|
|
|
|
+ .POST(HttpRequest.BodyPublishers.ofByteArray(payload))
|
|
|
|
|
+ .header("Content-Type", "application/json")
|
|
|
|
|
+ .timeout(Duration.ofSeconds(15));
|
|
|
|
|
+
|
|
|
|
|
+ if (needAuth) {
|
|
|
|
|
+ String currentToken = accessTokenSupplier.get();
|
|
|
|
|
+ if (currentToken != null && !currentToken.isBlank()) {
|
|
|
|
|
+ long timestamp = Instant.now().getEpochSecond();
|
|
|
|
|
+ String nonce = UUID.randomUUID().toString().replace("-", "").substring(0, 32);
|
|
|
|
|
+ reqBuilder.header("access-token", currentToken);
|
|
|
|
|
+ reqBuilder.header("timestamp", String.valueOf(timestamp));
|
|
|
|
|
+ reqBuilder.header("nonce", nonce);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return reqBuilder.build();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 处理 HTTP 响应,统一设置 record 状态。
|
|
|
|
|
+ */
|
|
|
|
|
+ private void handleResponse(MediaCallbackRecord record, HttpResponse<byte[]> response,
|
|
|
|
|
+ String qk, String actionType) {
|
|
|
|
|
+ int status = response.statusCode();
|
|
|
|
|
+ String responseBody = truncate(new String(response.body(), StandardCharsets.UTF_8), 4096);
|
|
|
|
|
+ record.setStatus(status);
|
|
|
|
|
+ record.setResponseBody(responseBody);
|
|
|
|
|
+
|
|
|
|
|
+ if (status >= 200 && status < 300) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ TencentResponse tencentResp = objectMapper.readValue(response.body(), TencentResponse.class);
|
|
|
|
|
+ record.setOk(tencentResp.isSuccess());
|
|
|
|
|
+ if (!tencentResp.isSuccess()) {
|
|
|
|
|
+ record.setErrorMessage("code=" + tencentResp.getCode() + " msg=" + tencentResp.getMessage());
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ record.setOk(false);
|
|
|
|
|
+ record.setErrorMessage("解析响应失败: " + e.getMessage());
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ record.setOk(false);
|
|
|
|
|
+ record.setErrorMessage("HTTP status: " + status);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ log.info("[Tencent] callback结果: qk={}, actionType={}, status={}, ok={}, response={}",
|
|
|
|
|
+ qk, actionType, status, record.isOk(), responseBody);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// ─── 构造 UserAction ────────────────────────────────────────────────────
|
|
// ─── 构造 UserAction ────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
private UserAction buildUserAction(BidRecord bid, PaymentInfo payment,
|
|
private UserAction buildUserAction(BidRecord bid, PaymentInfo payment,
|
|
@@ -277,7 +248,6 @@ public class TencentClient {
|
|
|
action.setActionParam(param);
|
|
action.setActionParam(param);
|
|
|
} else if ("CUSTOM".equals(actionType)) {
|
|
} else if ("CUSTOM".equals(actionType)) {
|
|
|
UserAction.ActionParam param = new UserAction.ActionParam();
|
|
UserAction.ActionParam param = new UserAction.ActionParam();
|
|
|
- // 根据百度 act 设置 custom_action
|
|
|
|
|
String customAction = switch (payment.getAct()) {
|
|
String customAction = switch (payment.getAct()) {
|
|
|
case 7 -> "key_behavior";
|
|
case 7 -> "key_behavior";
|
|
|
case 8 -> "7day_retention";
|
|
case 8 -> "7day_retention";
|
|
@@ -299,17 +269,16 @@ public class TencentClient {
|
|
|
Map<String, String> params = bid.getMediaParams();
|
|
Map<String, String> params = bid.getMediaParams();
|
|
|
if (params == null) return userId;
|
|
if (params == null) return userId;
|
|
|
|
|
|
|
|
- // 优先使用 MD5 哈希值(腾讯要求 hash_* 字段)
|
|
|
|
|
String imei = firstNotBlank(params.get("imei_md5"), params.get("imei"));
|
|
String imei = firstNotBlank(params.get("imei_md5"), params.get("imei"));
|
|
|
if (imei != null) userId.setHashImei(ensureMd5(imei));
|
|
if (imei != null) userId.setHashImei(ensureMd5(imei));
|
|
|
|
|
|
|
|
String idfa = firstNotBlank(params.get("idfa_md5"), params.get("idfa"));
|
|
String idfa = firstNotBlank(params.get("idfa_md5"), params.get("idfa"));
|
|
|
if (idfa != null) userId.setHashIdfa(ensureMd5(idfa));
|
|
if (idfa != null) userId.setHashIdfa(ensureMd5(idfa));
|
|
|
|
|
|
|
|
- String oaid = firstNotBlank(params.get("oaid_md5"), params.get("oaid"));
|
|
|
|
|
|
|
+ String oaid = firstNotBlank(params.get("hash_oaid"), params.get("oaid_md5"), params.get("oaid"));
|
|
|
if (oaid != null) userId.setHashOaid(ensureMd5(oaid));
|
|
if (oaid != null) userId.setHashOaid(ensureMd5(oaid));
|
|
|
|
|
|
|
|
- String androidId = firstNotBlank(params.get("android_id_md5"), params.get("android_id"));
|
|
|
|
|
|
|
+ String androidId = firstNotBlank(params.get("hash_android_id"), params.get("android_id_md5"), params.get("android_id"));
|
|
|
if (androidId != null) userId.setHashAndroidId(ensureMd5(androidId));
|
|
if (androidId != null) userId.setHashAndroidId(ensureMd5(androidId));
|
|
|
|
|
|
|
|
return userId;
|
|
return userId;
|
|
@@ -332,20 +301,14 @@ public class TencentClient {
|
|
|
|
|
|
|
|
// ─── 工具方法 ────────────────────────────────────────────────────────────
|
|
// ─── 工具方法 ────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
- /**
|
|
|
|
|
- * 确保值是 MD5 格式(32位十六进制)。
|
|
|
|
|
- * 如果不是,则计算 MD5。
|
|
|
|
|
- */
|
|
|
|
|
private static String ensureMd5(String value) {
|
|
private static String ensureMd5(String value) {
|
|
|
if (value == null || value.isBlank()) return null;
|
|
if (value == null || value.isBlank()) return null;
|
|
|
- // 如果已经是32位十六进制,直接返回
|
|
|
|
|
if (value.length() == 32 && value.matches("[0-9a-fA-F]+")) {
|
|
if (value.length() == 32 && value.matches("[0-9a-fA-F]+")) {
|
|
|
return value.toLowerCase();
|
|
return value.toLowerCase();
|
|
|
}
|
|
}
|
|
|
- // 否则计算 MD5
|
|
|
|
|
try {
|
|
try {
|
|
|
MessageDigest md = MessageDigest.getInstance("MD5");
|
|
MessageDigest md = MessageDigest.getInstance("MD5");
|
|
|
- byte[] hash = md.digest(value.getBytes(java.nio.charset.StandardCharsets.UTF_8));
|
|
|
|
|
|
|
+ byte[] hash = md.digest(value.getBytes(StandardCharsets.UTF_8));
|
|
|
return HexFormat.of().formatHex(hash);
|
|
return HexFormat.of().formatHex(hash);
|
|
|
} catch (Exception e) {
|
|
} catch (Exception e) {
|
|
|
return value;
|
|
return value;
|
|
@@ -370,6 +333,7 @@ public class TencentClient {
|
|
|
copy.setMedia(src.getMedia());
|
|
copy.setMedia(src.getMedia());
|
|
|
copy.setQk(src.getQk());
|
|
copy.setQk(src.getQk());
|
|
|
copy.setCallbackUrl(src.getCallbackUrl());
|
|
copy.setCallbackUrl(src.getCallbackUrl());
|
|
|
|
|
+ copy.setRequestBody(src.getRequestBody());
|
|
|
copy.setEventType(src.getEventType());
|
|
copy.setEventType(src.getEventType());
|
|
|
copy.setEventTimeMs(src.getEventTimeMs());
|
|
copy.setEventTimeMs(src.getEventTimeMs());
|
|
|
copy.setPurchase(src.getPurchase());
|
|
copy.setPurchase(src.getPurchase());
|