|
|
@@ -0,0 +1,232 @@
|
|
|
+package com.adx.tencent.conversionsync;
|
|
|
+
|
|
|
+import com.adx.tencent.baidu.model.PaymentInfo;
|
|
|
+import com.adx.tencent.storage.RedisHotStore;
|
|
|
+import com.adx.tencent.storage.TiDBColdStore;
|
|
|
+import com.adx.tencent.storage.model.BidRecord;
|
|
|
+import com.adx.tencent.storage.model.MediaCallbackRecord;
|
|
|
+import com.adx.tencent.tencent.TencentClient;
|
|
|
+import org.slf4j.Logger;
|
|
|
+import org.slf4j.LoggerFactory;
|
|
|
+import org.springframework.web.server.ResponseStatusException;
|
|
|
+
|
|
|
+import java.time.Instant;
|
|
|
+import java.util.LinkedHashMap;
|
|
|
+import java.util.LinkedHashSet;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.Set;
|
|
|
+
|
|
|
+import static org.springframework.http.HttpStatus.BAD_REQUEST;
|
|
|
+import static org.springframework.http.HttpStatus.NOT_FOUND;
|
|
|
+
|
|
|
+public class ManualTencentCallbackService {
|
|
|
+
|
|
|
+ private static final Logger log = LoggerFactory.getLogger(ManualTencentCallbackService.class);
|
|
|
+
|
|
|
+ private final TiDBColdStore coldStore;
|
|
|
+ private final RedisHotStore hotStore;
|
|
|
+ private final TencentClient tencentClient;
|
|
|
+
|
|
|
+ public ManualTencentCallbackService(TiDBColdStore coldStore,
|
|
|
+ RedisHotStore hotStore,
|
|
|
+ TencentClient tencentClient) {
|
|
|
+ this.coldStore = coldStore;
|
|
|
+ this.hotStore = hotStore;
|
|
|
+ this.tencentClient = tencentClient;
|
|
|
+ }
|
|
|
+
|
|
|
+ public Map<String, Object> replayDeductedCallback(long id) {
|
|
|
+ MediaCallbackRecord original = coldStore.getMediaCallbackById(id);
|
|
|
+ if (original == null) {
|
|
|
+ throw new ResponseStatusException(NOT_FOUND, "callback record not found");
|
|
|
+ }
|
|
|
+ if (!"tencent".equals(original.getMedia())) {
|
|
|
+ throw new ResponseStatusException(BAD_REQUEST, "callback record is not tencent");
|
|
|
+ }
|
|
|
+ if (!MediaCallbackRecord.DISPATCH_STATUS_DEDUCTED.equals(original.getDispatchStatus())) {
|
|
|
+ throw new ResponseStatusException(BAD_REQUEST, "callback record is not deducted");
|
|
|
+ }
|
|
|
+
|
|
|
+ MediaCallbackRecord replay = replayOnce(original);
|
|
|
+ replay.setId(original.getId());
|
|
|
+ coldStore.updateMediaCallback(replay);
|
|
|
+
|
|
|
+ Map<String, Object> result = new LinkedHashMap<>();
|
|
|
+ result.put("id", id);
|
|
|
+ result.put("dedupeKey", replay.getDedupeKey());
|
|
|
+ result.put("before", snapshot(original));
|
|
|
+ result.put("after", snapshot(replay));
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ private MediaCallbackRecord replayOnce(MediaCallbackRecord original) {
|
|
|
+ if (original.getRequestBody() != null && !original.getRequestBody().isBlank()) {
|
|
|
+ try {
|
|
|
+ MediaCallbackRecord replay = tencentClient.retryCallback(original);
|
|
|
+ replay.setDedupeKey(original.getDedupeKey());
|
|
|
+ replay.setAttempt(Math.max(original.getAttempt(), 1) + 1);
|
|
|
+ if (replay.getTrackingVersion() == null || replay.getTrackingVersion().isBlank()) {
|
|
|
+ replay.setTrackingVersion(original.getTrackingVersion());
|
|
|
+ }
|
|
|
+ return replay;
|
|
|
+ } catch (Exception e) {
|
|
|
+ MediaCallbackRecord failed = copyForFailure(original);
|
|
|
+ failed.setErrorMessage(e.getMessage());
|
|
|
+ return failed;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ BidRecord bid = hotStore.findBidByQk(original.getQk());
|
|
|
+ if (bid == null) {
|
|
|
+ bid = coldStore.getBidByQk(original.getQk());
|
|
|
+ }
|
|
|
+ if (bid == null) {
|
|
|
+ throw new ResponseStatusException(NOT_FOUND, "bid not found for qk=" + original.getQk());
|
|
|
+ }
|
|
|
+
|
|
|
+ DedupeInfo dedupeInfo = parseDedupeInfo(original);
|
|
|
+ String actionType = dedupeInfo.actionType != null && !dedupeInfo.actionType.isBlank()
|
|
|
+ ? dedupeInfo.actionType
|
|
|
+ : tencentClient.conversionActionType(original.getEventType());
|
|
|
+
|
|
|
+ patchBidForReplay(bid, original, dedupeInfo);
|
|
|
+
|
|
|
+ PaymentInfo payment = new PaymentInfo();
|
|
|
+ payment.setQk(original.getQk());
|
|
|
+ payment.setDate(dedupeInfo.date);
|
|
|
+ payment.setDeviceId(dedupeInfo.deviceId);
|
|
|
+ payment.setAct(original.getEventType());
|
|
|
+ payment.setPayment(original.getPurchase());
|
|
|
+ payment.setGmv(original.getPurchase());
|
|
|
+
|
|
|
+ MediaCallbackRecord replay = tencentClient.sendConversionEvent(
|
|
|
+ bid,
|
|
|
+ payment,
|
|
|
+ resolvePlatform(bid),
|
|
|
+ actionType
|
|
|
+ );
|
|
|
+ replay.setDedupeKey(original.getDedupeKey());
|
|
|
+ replay.setAttempt(Math.max(original.getAttempt(), 1) + 1);
|
|
|
+ if (replay.getTrackingVersion() == null || replay.getTrackingVersion().isBlank()) {
|
|
|
+ replay.setTrackingVersion(original.getTrackingVersion());
|
|
|
+ }
|
|
|
+ replay.setDispatchStatus(MediaCallbackRecord.DISPATCH_STATUS_SENT);
|
|
|
+ replay.setCreatedAt(Instant.now());
|
|
|
+ log.info("[ManualTencentCallback] manual replay sent | qk={} | dedupeKey={} | ok={} | status={}",
|
|
|
+ original.getQk(), original.getDedupeKey(), replay.isOk(), replay.getStatus());
|
|
|
+ return replay;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void patchBidForReplay(BidRecord bid, MediaCallbackRecord original, DedupeInfo dedupeInfo) {
|
|
|
+ if ((bid.getTagId() == null || bid.getTagId().isBlank()) && dedupeInfo.tagId != null && !dedupeInfo.tagId.isBlank()) {
|
|
|
+ bid.setTagId(dedupeInfo.tagId);
|
|
|
+ }
|
|
|
+
|
|
|
+ Map<String, String> params = bid.getMediaParams();
|
|
|
+ if (params == null) {
|
|
|
+ params = new LinkedHashMap<>();
|
|
|
+ bid.setMediaParams(params);
|
|
|
+ } else if (!(params instanceof LinkedHashMap<String, String>)) {
|
|
|
+ params = new LinkedHashMap<>(params);
|
|
|
+ bid.setMediaParams(params);
|
|
|
+ }
|
|
|
+ if ((params.get("callback") == null || params.get("callback").isBlank())
|
|
|
+ && original.getCallbackUrl() != null && !original.getCallbackUrl().isBlank()) {
|
|
|
+ params.put("callback", original.getCallbackUrl());
|
|
|
+ }
|
|
|
+ if ((params.get(BidRecord.TRACKING_VERSION_KEY) == null || params.get(BidRecord.TRACKING_VERSION_KEY).isBlank())
|
|
|
+ && original.getTrackingVersion() != null && !original.getTrackingVersion().isBlank()) {
|
|
|
+ params.put(BidRecord.TRACKING_VERSION_KEY, original.getTrackingVersion());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static MediaCallbackRecord copyForFailure(MediaCallbackRecord original) {
|
|
|
+ MediaCallbackRecord failed = new MediaCallbackRecord();
|
|
|
+ failed.setDedupeKey(original.getDedupeKey());
|
|
|
+ failed.setMedia(original.getMedia());
|
|
|
+ failed.setQk(original.getQk());
|
|
|
+ failed.setCallbackUrl(original.getCallbackUrl());
|
|
|
+ failed.setRequestBody(original.getRequestBody());
|
|
|
+ failed.setEventType(original.getEventType());
|
|
|
+ failed.setEventTimeMs(Instant.now().toEpochMilli());
|
|
|
+ failed.setPurchase(original.getPurchase());
|
|
|
+ failed.setStatus(original.getStatus());
|
|
|
+ failed.setOk(false);
|
|
|
+ failed.setAttempt(Math.max(original.getAttempt(), 1) + 1);
|
|
|
+ failed.setResponseBody(original.getResponseBody());
|
|
|
+ failed.setDispatchStatus(MediaCallbackRecord.DISPATCH_STATUS_SENT);
|
|
|
+ failed.setTrackingVersion(original.getTrackingVersion());
|
|
|
+ failed.setCreatedAt(Instant.now());
|
|
|
+ return failed;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static DedupeInfo parseDedupeInfo(MediaCallbackRecord record) {
|
|
|
+ DedupeInfo info = new DedupeInfo();
|
|
|
+ String dedupeKey = record.getDedupeKey();
|
|
|
+ if (dedupeKey == null || dedupeKey.isBlank()) {
|
|
|
+ return info;
|
|
|
+ }
|
|
|
+
|
|
|
+ String[] parts = dedupeKey.split(":");
|
|
|
+ if (parts.length < 7) {
|
|
|
+ return info;
|
|
|
+ }
|
|
|
+ info.date = parts[2];
|
|
|
+ info.deviceId = parts[3];
|
|
|
+ info.tagId = parts[4];
|
|
|
+ info.actionType = joinTail(parts, 6);
|
|
|
+ return info;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static String joinTail(String[] parts, int start) {
|
|
|
+ if (parts.length <= start) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ StringBuilder builder = new StringBuilder();
|
|
|
+ for (int i = start; i < parts.length; i++) {
|
|
|
+ if (i > start) {
|
|
|
+ builder.append(':');
|
|
|
+ }
|
|
|
+ builder.append(parts[i]);
|
|
|
+ }
|
|
|
+ return builder.toString();
|
|
|
+ }
|
|
|
+
|
|
|
+ private static String resolvePlatform(BidRecord bid) {
|
|
|
+ if (bid.getPlatform() != null && !bid.getPlatform().isBlank()) {
|
|
|
+ return bid.getPlatform().toLowerCase();
|
|
|
+ }
|
|
|
+ if (bid.getMediaParams() == null) {
|
|
|
+ return "android";
|
|
|
+ }
|
|
|
+ String platform = bid.getMediaParams().get("baidu_platform");
|
|
|
+ if (platform != null && !platform.isBlank()) {
|
|
|
+ return platform.toLowerCase();
|
|
|
+ }
|
|
|
+ String idfa = bid.getMediaParams().get("idfa");
|
|
|
+ if (idfa != null && !idfa.isBlank()) {
|
|
|
+ return "ios";
|
|
|
+ }
|
|
|
+ return "android";
|
|
|
+ }
|
|
|
+
|
|
|
+ private static Map<String, Object> snapshot(MediaCallbackRecord record) {
|
|
|
+ Map<String, Object> data = new LinkedHashMap<>();
|
|
|
+ data.put("id", record.getId());
|
|
|
+ data.put("dispatchStatus", record.getDispatchStatus());
|
|
|
+ data.put("attempt", record.getAttempt());
|
|
|
+ data.put("status", record.getStatus());
|
|
|
+ data.put("ok", record.isOk());
|
|
|
+ data.put("errorMessage", record.getErrorMessage());
|
|
|
+ data.put("callbackUrl", record.getCallbackUrl());
|
|
|
+ data.put("trackingVersion", record.getTrackingVersion());
|
|
|
+ return data;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static class DedupeInfo {
|
|
|
+ private String date;
|
|
|
+ private String deviceId;
|
|
|
+ private String tagId;
|
|
|
+ private String actionType;
|
|
|
+ }
|
|
|
+}
|