|
@@ -14,7 +14,11 @@ import org.slf4j.LoggerFactory;
|
|
|
|
|
|
|
|
import java.time.Instant;
|
|
import java.time.Instant;
|
|
|
import java.util.ArrayList;
|
|
import java.util.ArrayList;
|
|
|
|
|
+import java.util.HashSet;
|
|
|
|
|
+import java.util.LinkedHashSet;
|
|
|
import java.util.List;
|
|
import java.util.List;
|
|
|
|
|
+import java.util.Map;
|
|
|
|
|
+import java.util.Set;
|
|
|
import java.util.concurrent.*;
|
|
import java.util.concurrent.*;
|
|
|
import java.util.concurrent.atomic.AtomicInteger;
|
|
import java.util.concurrent.atomic.AtomicInteger;
|
|
|
|
|
|
|
@@ -94,122 +98,129 @@ public class ConversionSyncService {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private void processPayments(List<PaymentInfo> payments, SyncResult result, SyncMode mode) throws Exception {
|
|
private void processPayments(List<PaymentInfo> payments, SyncResult result, SyncMode mode) throws Exception {
|
|
|
|
|
+ if (payments == null || payments.isEmpty()) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
ExecutorService executor = mode == SyncMode.BACKFILL ? backfillExecutor : realtimeExecutor;
|
|
ExecutorService executor = mode == SyncMode.BACKFILL ? backfillExecutor : realtimeExecutor;
|
|
|
- List<CompletableFuture<Void>> futures = new ArrayList<>(payments.size());
|
|
|
|
|
|
|
+ Set<String> qks = new LinkedHashSet<>();
|
|
|
for (PaymentInfo payment : payments) {
|
|
for (PaymentInfo payment : payments) {
|
|
|
- futures.add(CompletableFuture.runAsync(() -> processSinglePayment(payment, result), executor));
|
|
|
|
|
|
|
+ result.fetched.incrementAndGet();
|
|
|
|
|
+ if (payment.getQk() != null && !payment.getQk().isBlank()) {
|
|
|
|
|
+ qks.add(payment.getQk());
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
- CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
|
|
|
|
|
- }
|
|
|
|
|
|
|
|
|
|
- private void processSinglePayment(PaymentInfo payment, SyncResult result) {
|
|
|
|
|
- result.fetched.incrementAndGet();
|
|
|
|
|
|
|
+ Map<String, BidRecord> bidMap = hotStore.findBidsByQks(new ArrayList<>(qks));
|
|
|
|
|
+ List<ConversionRecord> conversionRecords = new ArrayList<>();
|
|
|
|
|
+ List<CallbackWork> callbackWorks = new ArrayList<>();
|
|
|
|
|
|
|
|
- // 用 qk 查找 bid
|
|
|
|
|
- BidRecord bid;
|
|
|
|
|
- try {
|
|
|
|
|
- bid = hotStore.findBidByQk(payment.getQk());
|
|
|
|
|
- } catch (Exception e) {
|
|
|
|
|
- log.warn("[ConversionSync] hot bid lookup failed, fallback to cold store: qk={}, {}", payment.getQk(), e.getMessage());
|
|
|
|
|
- bid = coldStore != null ? coldStore.getBidByQk(payment.getQk()) : null;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ for (PaymentInfo payment : payments) {
|
|
|
|
|
+ BidRecord bid = bidMap.get(payment.getQk());
|
|
|
|
|
+ if (bid == null) {
|
|
|
|
|
+ conversionRecords.add(conversionRecord(payment, null, ""));
|
|
|
|
|
+ result.skipped.incrementAndGet();
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- if (bid == null) {
|
|
|
|
|
- if (coldStore != null) {
|
|
|
|
|
- try {
|
|
|
|
|
- coldStore.saveConversion(conversionRecord(payment, null, ""));
|
|
|
|
|
- } catch (Exception e) {
|
|
|
|
|
- log.error("[ConversionSync] save conversion failed: qk={}, act={}, error={}",
|
|
|
|
|
- payment.getQk(), payment.getAct(), e.getMessage(), e);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if (!"tencent".equals(bid.getMedia())) {
|
|
|
|
|
+ conversionRecords.add(conversionRecord(payment, bid, bid.getMedia()));
|
|
|
|
|
+ result.skipped.incrementAndGet();
|
|
|
|
|
+ continue;
|
|
|
}
|
|
}
|
|
|
- result.skipped.incrementAndGet();
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
|
|
|
|
|
|
|
+ result.matched.incrementAndGet();
|
|
|
|
|
+ log.info("[ConversionSync] 匹配到腾讯转化 | qk={} | act={} | deviceId={} | date={} | payment={}",
|
|
|
|
|
+ payment.getQk(), payment.getAct(), payment.getDeviceId(), payment.getDate(), payment.getPayment());
|
|
|
|
|
+ conversionRecords.add(conversionRecord(payment, bid, bid.getMedia()));
|
|
|
|
|
|
|
|
- // bid 不是腾讯,或没有设备标识
|
|
|
|
|
- if (!"tencent".equals(bid.getMedia())) {
|
|
|
|
|
- if (coldStore != null) {
|
|
|
|
|
- try {
|
|
|
|
|
- coldStore.saveConversion(conversionRecord(payment, bid, bid.getMedia()));
|
|
|
|
|
- } catch (Exception e) {
|
|
|
|
|
- log.error("[ConversionSync] save conversion failed: qk={}, act={}, error={}",
|
|
|
|
|
- payment.getQk(), payment.getAct(), e.getMessage(), e);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ String actionType = null;
|
|
|
|
|
+ if (tagEventResolver != null) {
|
|
|
|
|
+ actionType = tagEventResolver.resolveActionType(bid.getTagId(), payment.getAct());
|
|
|
}
|
|
}
|
|
|
- result.skipped.incrementAndGet();
|
|
|
|
|
- return;
|
|
|
|
|
|
|
+ if (actionType == null || actionType.isBlank()) {
|
|
|
|
|
+ actionType = tencentClient.conversionActionType(payment.getAct());
|
|
|
|
|
+ log.warn("[ConversionSync] 广告位回传方式未配置, 回退act-map | qk={} | tagId={} | act={} | actionType={}",
|
|
|
|
|
+ payment.getQk(), bid.getTagId(), payment.getAct(), actionType);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ callbackWorks.add(new CallbackWork(payment, bid, actionType, callbackDedupeKey(payment, bid, actionType)));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- result.matched.incrementAndGet();
|
|
|
|
|
- log.info("[ConversionSync] 匹配到腾讯转化 | qk={} | act={} | deviceId={} | date={} | payment={}",
|
|
|
|
|
- payment.getQk(), payment.getAct(), payment.getDeviceId(), payment.getDate(), payment.getPayment());
|
|
|
|
|
- if (coldStore != null) {
|
|
|
|
|
|
|
+ if (coldStore != null && !conversionRecords.isEmpty()) {
|
|
|
try {
|
|
try {
|
|
|
- coldStore.saveConversion(conversionRecord(payment, bid, bid.getMedia()));
|
|
|
|
|
|
|
+ coldStore.saveConversions(conversionRecords);
|
|
|
} catch (Exception e) {
|
|
} catch (Exception e) {
|
|
|
- log.error("[ConversionSync] save conversion failed: qk={}, act={}, error={}",
|
|
|
|
|
- payment.getQk(), payment.getAct(), e.getMessage(), e);
|
|
|
|
|
|
|
+ log.error("[ConversionSync] batch save conversions failed: size={}, {}", conversionRecords.size(), e.getMessage(), e);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- String actionType = null;
|
|
|
|
|
- if (tagEventResolver != null) {
|
|
|
|
|
- actionType = tagEventResolver.resolveActionType(bid.getTagId(), payment.getAct());
|
|
|
|
|
- }
|
|
|
|
|
- if (actionType == null || actionType.isBlank()) {
|
|
|
|
|
- actionType = tencentClient.conversionActionType(payment.getAct());
|
|
|
|
|
- log.warn("[ConversionSync] 广告位回传方式未配置, 回退act-map | qk={} | tagId={} | act={} | actionType={}",
|
|
|
|
|
- payment.getQk(), bid.getTagId(), payment.getAct(), actionType);
|
|
|
|
|
|
|
+ if (callbackWorks.isEmpty()) {
|
|
|
|
|
+ return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 幂等检查
|
|
|
|
|
- String callbackKey = callbackDedupeKey(payment, bid, actionType);
|
|
|
|
|
|
|
+ Set<String> existing = new HashSet<>();
|
|
|
if (coldStore != null) {
|
|
if (coldStore != null) {
|
|
|
try {
|
|
try {
|
|
|
- String legacyCallbackKey = legacyCallbackDedupeKey(payment, bid.getMedia());
|
|
|
|
|
- boolean exists = coldStore.successfulCallbackExistsForEvent(
|
|
|
|
|
- callbackKey, legacyCallbackKey, payment.getAct());
|
|
|
|
|
- if (exists) {
|
|
|
|
|
- result.alreadySent.incrementAndGet();
|
|
|
|
|
- return;
|
|
|
|
|
|
|
+ List<String> callbackKeys = new ArrayList<>(callbackWorks.size());
|
|
|
|
|
+ List<String> legacyKeys = new ArrayList<>(callbackWorks.size());
|
|
|
|
|
+ List<Integer> eventTypes = new ArrayList<>(callbackWorks.size());
|
|
|
|
|
+ for (CallbackWork work : callbackWorks) {
|
|
|
|
|
+ callbackKeys.add(work.callbackKey);
|
|
|
|
|
+ legacyKeys.add(legacyCallbackDedupeKey(work.payment, work.bid.getMedia()));
|
|
|
|
|
+ eventTypes.add(work.payment.getAct());
|
|
|
}
|
|
}
|
|
|
|
|
+ existing.addAll(coldStore.successfulCallbackKeysWithLegacy(callbackKeys, legacyKeys, eventTypes));
|
|
|
} catch (Exception e) {
|
|
} catch (Exception e) {
|
|
|
- log.error("[ConversionSync] dedup check error: key={}, {}", callbackKey, e.getMessage());
|
|
|
|
|
|
|
+ log.error("[ConversionSync] batch dedup check error: size={}, {}", callbackWorks.size(), e.getMessage(), e);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- String platform = resolvePlatform(bid);
|
|
|
|
|
- MediaCallbackRecord callbackRecord = tencentClient.sendConversionEvent(bid, payment, platform, actionType);
|
|
|
|
|
- callbackRecord.setDedupeKey(callbackKey);
|
|
|
|
|
-
|
|
|
|
|
- if (callbackRecord.getStatus() == 0 && callbackRecord.getErrorMessage() != null) {
|
|
|
|
|
- // 网络级错误
|
|
|
|
|
- result.failed.incrementAndGet();
|
|
|
|
|
- if (coldStore != null) {
|
|
|
|
|
- try {
|
|
|
|
|
- coldStore.saveMediaCallback(callbackRecord);
|
|
|
|
|
- } catch (Exception e) {
|
|
|
|
|
- log.error("[ConversionSync] 保存callback记录失败: dedupeKey={}, error={}",
|
|
|
|
|
- callbackKey, e.getMessage());
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ List<CallbackWork> toSend = new ArrayList<>(callbackWorks.size());
|
|
|
|
|
+ for (CallbackWork work : callbackWorks) {
|
|
|
|
|
+ if (existing.contains(work.callbackKey)) {
|
|
|
|
|
+ result.alreadySent.incrementAndGet();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ toSend.add(work);
|
|
|
}
|
|
}
|
|
|
- return;
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // HTTP 请求完成
|
|
|
|
|
- result.sent.incrementAndGet();
|
|
|
|
|
- if (coldStore != null) {
|
|
|
|
|
|
|
+ List<CompletableFuture<MediaCallbackRecord>> futures = new ArrayList<>(toSend.size());
|
|
|
|
|
+ for (CallbackWork work : toSend) {
|
|
|
|
|
+ futures.add(CompletableFuture.supplyAsync(() -> sendConversionEvent(work), executor));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ List<MediaCallbackRecord> callbackRecords = new ArrayList<>(toSend.size());
|
|
|
|
|
+ for (CompletableFuture<MediaCallbackRecord> future : futures) {
|
|
|
|
|
+ MediaCallbackRecord callbackRecord = future.join();
|
|
|
|
|
+ if (callbackRecord == null) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (callbackRecord.getStatus() == 0 && callbackRecord.getErrorMessage() != null) {
|
|
|
|
|
+ result.failed.incrementAndGet();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ result.sent.incrementAndGet();
|
|
|
|
|
+ }
|
|
|
|
|
+ callbackRecords.add(callbackRecord);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (coldStore != null && !callbackRecords.isEmpty()) {
|
|
|
try {
|
|
try {
|
|
|
- coldStore.saveMediaCallback(callbackRecord);
|
|
|
|
|
|
|
+ coldStore.saveMediaCallbacks(callbackRecords);
|
|
|
} catch (Exception e) {
|
|
} catch (Exception e) {
|
|
|
- log.error("[ConversionSync] 保存callback记录失败: dedupeKey={}, error={}",
|
|
|
|
|
- callbackKey, e.getMessage());
|
|
|
|
|
|
|
+ log.error("[ConversionSync] batch save callbacks failed: size={}, {}", callbackRecords.size(), e.getMessage(), e);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private MediaCallbackRecord sendConversionEvent(CallbackWork work) {
|
|
|
|
|
+ String platform = resolvePlatform(work.bid);
|
|
|
|
|
+ MediaCallbackRecord callbackRecord = tencentClient.sendConversionEvent(
|
|
|
|
|
+ work.bid, work.payment, platform, work.actionType);
|
|
|
|
|
+ callbackRecord.setDedupeKey(work.callbackKey);
|
|
|
|
|
+ return callbackRecord;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// ─── 辅助方法 ─────────────────────────────────────────────────────────────
|
|
// ─── 辅助方法 ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -270,6 +281,20 @@ public class ConversionSyncService {
|
|
|
media, p.getQk(), p.getDate(), p.getDeviceId(), p.getAct(), p.getAct());
|
|
media, p.getQk(), p.getDate(), p.getDeviceId(), p.getAct(), p.getAct());
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private static class CallbackWork {
|
|
|
|
|
+ private final PaymentInfo payment;
|
|
|
|
|
+ private final BidRecord bid;
|
|
|
|
|
+ private final String actionType;
|
|
|
|
|
+ private final String callbackKey;
|
|
|
|
|
+
|
|
|
|
|
+ private CallbackWork(PaymentInfo payment, BidRecord bid, String actionType, String callbackKey) {
|
|
|
|
|
+ this.payment = payment;
|
|
|
|
|
+ this.bid = bid;
|
|
|
|
|
+ this.actionType = actionType;
|
|
|
|
|
+ this.callbackKey = callbackKey;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// ─── Result DTO ─────────────────────────────────────────────────────────
|
|
// ─── Result DTO ─────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
public static class SyncResult {
|
|
public static class SyncResult {
|