|
@@ -11,6 +11,13 @@ import com.adx.tencent.honor.model.HonorMediaCallbackRecord;
|
|
|
import com.adx.tencent.honor.service.HonorTagEventResolver;
|
|
import com.adx.tencent.honor.service.HonorTagEventResolver;
|
|
|
import com.adx.tencent.honor.store.HonorColdStore;
|
|
import com.adx.tencent.honor.store.HonorColdStore;
|
|
|
import com.adx.tencent.honor.store.HonorHotStore;
|
|
import com.adx.tencent.honor.store.HonorHotStore;
|
|
|
|
|
+import com.adx.tencent.kuaishou.client.KuaishouClient;
|
|
|
|
|
+import com.adx.tencent.kuaishou.model.KuaishouBidRecord;
|
|
|
|
|
+import com.adx.tencent.kuaishou.model.KuaishouMediaCallbackRecord;
|
|
|
|
|
+import com.adx.tencent.kuaishou.model.KuaishouTagEventRecord;
|
|
|
|
|
+import com.adx.tencent.kuaishou.service.KuaishouTagEventResolver;
|
|
|
|
|
+import com.adx.tencent.kuaishou.store.KuaishouColdStore;
|
|
|
|
|
+import com.adx.tencent.kuaishou.store.KuaishouHotStore;
|
|
|
import com.adx.tencent.storage.RedisHotStore;
|
|
import com.adx.tencent.storage.RedisHotStore;
|
|
|
import com.adx.tencent.storage.TiDBColdStore;
|
|
import com.adx.tencent.storage.TiDBColdStore;
|
|
|
import com.adx.tencent.storage.model.BidRecord;
|
|
import com.adx.tencent.storage.model.BidRecord;
|
|
@@ -37,7 +44,7 @@ import java.util.concurrent.atomic.AtomicInteger;
|
|
|
/**
|
|
/**
|
|
|
* 统一转化同步:
|
|
* 统一转化同步:
|
|
|
* 1. 只从百度拉一次转化
|
|
* 1. 只从百度拉一次转化
|
|
|
- * 2. 按 qk 匹配腾讯/荣耀 bid
|
|
|
|
|
|
|
+ * 2. 按 qk 匹配腾讯/荣耀/快手 bid
|
|
|
* 3. 百度转化明细统一落 tencent_baidu_conversions
|
|
* 3. 百度转化明细统一落 tencent_baidu_conversions
|
|
|
* 4. 媒体回传结果分别落各自表
|
|
* 4. 媒体回传结果分别落各自表
|
|
|
*/
|
|
*/
|
|
@@ -50,12 +57,16 @@ public class ConversionSyncService {
|
|
|
private final ConversionClient baiduClient;
|
|
private final ConversionClient baiduClient;
|
|
|
private final RedisHotStore hotStore;
|
|
private final RedisHotStore hotStore;
|
|
|
private final HonorHotStore honorHotStore;
|
|
private final HonorHotStore honorHotStore;
|
|
|
|
|
+ private final KuaishouHotStore kuaishouHotStore;
|
|
|
private final TencentClient tencentClient;
|
|
private final TencentClient tencentClient;
|
|
|
private final TiDBColdStore coldStore;
|
|
private final TiDBColdStore coldStore;
|
|
|
private final HonorColdStore honorColdStore;
|
|
private final HonorColdStore honorColdStore;
|
|
|
|
|
+ private final KuaishouColdStore kuaishouColdStore;
|
|
|
private final HonorClient honorClient;
|
|
private final HonorClient honorClient;
|
|
|
|
|
+ private final KuaishouClient kuaishouClient;
|
|
|
private final TagEventResolver tagEventResolver;
|
|
private final TagEventResolver tagEventResolver;
|
|
|
private final HonorTagEventResolver honorTagEventResolver;
|
|
private final HonorTagEventResolver honorTagEventResolver;
|
|
|
|
|
+ private final KuaishouTagEventResolver kuaishouTagEventResolver;
|
|
|
private final ExecutorService realtimeExecutor;
|
|
private final ExecutorService realtimeExecutor;
|
|
|
private final ExecutorService backfillExecutor;
|
|
private final ExecutorService backfillExecutor;
|
|
|
|
|
|
|
@@ -70,9 +81,13 @@ public class ConversionSyncService {
|
|
|
TiDBColdStore coldStore,
|
|
TiDBColdStore coldStore,
|
|
|
TagEventResolver tagEventResolver,
|
|
TagEventResolver tagEventResolver,
|
|
|
HonorTagEventResolver honorTagEventResolver,
|
|
HonorTagEventResolver honorTagEventResolver,
|
|
|
|
|
+ KuaishouTagEventResolver kuaishouTagEventResolver,
|
|
|
HonorHotStore honorHotStore,
|
|
HonorHotStore honorHotStore,
|
|
|
HonorColdStore honorColdStore,
|
|
HonorColdStore honorColdStore,
|
|
|
|
|
+ KuaishouHotStore kuaishouHotStore,
|
|
|
|
|
+ KuaishouColdStore kuaishouColdStore,
|
|
|
HonorClient honorClient,
|
|
HonorClient honorClient,
|
|
|
|
|
+ KuaishouClient kuaishouClient,
|
|
|
AppProperties props) {
|
|
AppProperties props) {
|
|
|
this.baiduClient = baiduClient;
|
|
this.baiduClient = baiduClient;
|
|
|
this.hotStore = hotStore;
|
|
this.hotStore = hotStore;
|
|
@@ -80,9 +95,13 @@ public class ConversionSyncService {
|
|
|
this.coldStore = coldStore;
|
|
this.coldStore = coldStore;
|
|
|
this.tagEventResolver = tagEventResolver;
|
|
this.tagEventResolver = tagEventResolver;
|
|
|
this.honorTagEventResolver = honorTagEventResolver;
|
|
this.honorTagEventResolver = honorTagEventResolver;
|
|
|
|
|
+ this.kuaishouTagEventResolver = kuaishouTagEventResolver;
|
|
|
this.honorHotStore = honorHotStore;
|
|
this.honorHotStore = honorHotStore;
|
|
|
this.honorColdStore = honorColdStore;
|
|
this.honorColdStore = honorColdStore;
|
|
|
|
|
+ this.kuaishouHotStore = kuaishouHotStore;
|
|
|
|
|
+ this.kuaishouColdStore = kuaishouColdStore;
|
|
|
this.honorClient = honorClient;
|
|
this.honorClient = honorClient;
|
|
|
|
|
+ this.kuaishouClient = kuaishouClient;
|
|
|
this.realtimeExecutor = Executors.newFixedThreadPool(
|
|
this.realtimeExecutor = Executors.newFixedThreadPool(
|
|
|
REALTIME_CONCURRENCY,
|
|
REALTIME_CONCURRENCY,
|
|
|
r -> {
|
|
r -> {
|
|
@@ -149,7 +168,7 @@ public class ConversionSyncService {
|
|
|
|
|
|
|
|
ExecutorService executor = mode == SyncMode.BACKFILL ? backfillExecutor : realtimeExecutor;
|
|
ExecutorService executor = mode == SyncMode.BACKFILL ? backfillExecutor : realtimeExecutor;
|
|
|
|
|
|
|
|
- // 1. 批量查这一页转化对应的腾讯/荣耀 bid;Redis miss 时会自动回落到冷库。
|
|
|
|
|
|
|
+ // 1. 批量查这一页转化对应的腾讯/荣耀/快手 bid;Redis miss 时会自动回落到冷库。
|
|
|
BidLookup lookup = loadBidLookup(payments, result);
|
|
BidLookup lookup = loadBidLookup(payments, result);
|
|
|
|
|
|
|
|
// 2. 统一整理要落库的百度转化明细,以及要回传给媒体的任务。
|
|
// 2. 统一整理要落库的百度转化明细,以及要回传给媒体的任务。
|
|
@@ -191,7 +210,10 @@ public class ConversionSyncService {
|
|
|
Map<String, HonorBidRecord> honorBids = honorHotStore != null
|
|
Map<String, HonorBidRecord> honorBids = honorHotStore != null
|
|
|
? honorHotStore.findBidsByQks(qkList)
|
|
? honorHotStore.findBidsByQks(qkList)
|
|
|
: Map.of();
|
|
: Map.of();
|
|
|
- return new BidLookup(tencentBids, honorBids);
|
|
|
|
|
|
|
+ Map<String, KuaishouBidRecord> kuaishouBids = kuaishouHotStore != null
|
|
|
|
|
+ ? kuaishouHotStore.findBidsByQks(qkList)
|
|
|
|
|
+ : Map.of();
|
|
|
|
|
+ return new BidLookup(tencentBids, honorBids, kuaishouBids);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private void backfillTencentAccountIds(List<String> qkList, Map<String, BidRecord> tencentBids) {
|
|
private void backfillTencentAccountIds(List<String> qkList, Map<String, BidRecord> tencentBids) {
|
|
@@ -245,8 +267,9 @@ public class ConversionSyncService {
|
|
|
for (PaymentInfo payment : payments) {
|
|
for (PaymentInfo payment : payments) {
|
|
|
BidRecord tencentBid = lookup.tencentBids.get(payment.getQk());
|
|
BidRecord tencentBid = lookup.tencentBids.get(payment.getQk());
|
|
|
HonorBidRecord honorBid = lookup.honorBids.get(payment.getQk());
|
|
HonorBidRecord honorBid = lookup.honorBids.get(payment.getQk());
|
|
|
|
|
+ KuaishouBidRecord kuaishouBid = lookup.kuaishouBids.get(payment.getQk());
|
|
|
|
|
|
|
|
- if (tencentBid == null && honorBid == null) {
|
|
|
|
|
|
|
+ if (tencentBid == null && honorBid == null && kuaishouBid == null) {
|
|
|
conversionRecords.add(toConversionRecord(payment, null, ""));
|
|
conversionRecords.add(toConversionRecord(payment, null, ""));
|
|
|
result.skipped.incrementAndGet();
|
|
result.skipped.incrementAndGet();
|
|
|
continue;
|
|
continue;
|
|
@@ -254,8 +277,10 @@ public class ConversionSyncService {
|
|
|
|
|
|
|
|
boolean hasTencentBid = tencentBid != null;
|
|
boolean hasTencentBid = tencentBid != null;
|
|
|
boolean hasHonorBid = honorBid != null;
|
|
boolean hasHonorBid = honorBid != null;
|
|
|
|
|
+ boolean hasKuaishouBid = kuaishouBid != null;
|
|
|
boolean validTencent = hasTencentBid && "tencent".equals(tencentBid.getMedia());
|
|
boolean validTencent = hasTencentBid && "tencent".equals(tencentBid.getMedia());
|
|
|
boolean validHonor = hasHonorBid && "honor".equals(honorBid.getMedia());
|
|
boolean validHonor = hasHonorBid && "honor".equals(honorBid.getMedia());
|
|
|
|
|
+ boolean validKuaishou = hasKuaishouBid && "kuaishou".equals(kuaishouBid.getMedia());
|
|
|
|
|
|
|
|
if (hasTencentBid && !validTencent) {
|
|
if (hasTencentBid && !validTencent) {
|
|
|
log.warn("[ConversionSync] 腾讯 bid media 非法 | qk={} | media={}", payment.getQk(), tencentBid.getMedia());
|
|
log.warn("[ConversionSync] 腾讯 bid media 非法 | qk={} | media={}", payment.getQk(), tencentBid.getMedia());
|
|
@@ -265,8 +290,13 @@ public class ConversionSyncService {
|
|
|
log.warn("[ConversionSync] 荣耀 bid media 非法 | qk={} | media={}", payment.getQk(), honorBid.getMedia());
|
|
log.warn("[ConversionSync] 荣耀 bid media 非法 | qk={} | media={}", payment.getQk(), honorBid.getMedia());
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- if (validTencent && validHonor) {
|
|
|
|
|
- log.warn("[ConversionSync] qk 同时命中腾讯和荣耀,跳过分发 | qk={} | act={} | date={}",
|
|
|
|
|
|
|
+ if (hasKuaishouBid && !validKuaishou) {
|
|
|
|
|
+ log.warn("[ConversionSync] 快手 bid media 非法 | qk={} | media={}", payment.getQk(), kuaishouBid.getMedia());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ int matchedMediaCount = (validTencent ? 1 : 0) + (validHonor ? 1 : 0) + (validKuaishou ? 1 : 0);
|
|
|
|
|
+ if (matchedMediaCount > 1) {
|
|
|
|
|
+ log.warn("[ConversionSync] qk 同时命中多媒体,跳过分发 | qk={} | act={} | date={}",
|
|
|
payment.getQk(), payment.getAct(), payment.getDate());
|
|
payment.getQk(), payment.getAct(), payment.getDate());
|
|
|
result.skipped.incrementAndGet();
|
|
result.skipped.incrementAndGet();
|
|
|
continue;
|
|
continue;
|
|
@@ -286,6 +316,19 @@ public class ConversionSyncService {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if (!validHonor) {
|
|
if (!validHonor) {
|
|
|
|
|
+ if (validKuaishou) {
|
|
|
|
|
+ conversionRecords.add(toKuaishouConversionRecord(payment, kuaishouBid));
|
|
|
|
|
+ result.matched.incrementAndGet();
|
|
|
|
|
+ log.info("[ConversionSync] 匹配到快手转化 | qk={} | act={} | deviceId={} | date={} | payment={}",
|
|
|
|
|
+ payment.getQk(), payment.getAct(), payment.getDeviceId(), payment.getDate(), payment.getPayment());
|
|
|
|
|
+
|
|
|
|
|
+ CallbackTask kuaishouTask = buildKuaishouCallbackTask(payment, kuaishouBid);
|
|
|
|
|
+ if (kuaishouTask != null) {
|
|
|
|
|
+ callbackTasks.add(kuaishouTask);
|
|
|
|
|
+ }
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
conversionRecords.add(toConversionRecord(payment, hasTencentBid ? tencentBid : null, hasTencentBid ? tencentBid.getMedia() : ""));
|
|
conversionRecords.add(toConversionRecord(payment, hasTencentBid ? tencentBid : null, hasTencentBid ? tencentBid.getMedia() : ""));
|
|
|
result.skipped.incrementAndGet();
|
|
result.skipped.incrementAndGet();
|
|
|
continue;
|
|
continue;
|
|
@@ -351,6 +394,26 @@ public class ConversionSyncService {
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private CallbackTask buildKuaishouCallbackTask(PaymentInfo payment, KuaishouBidRecord bid) {
|
|
|
|
|
+ if (kuaishouColdStore == null && kuaishouTagEventResolver == null) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ KuaishouTagEventRecord tagEvent = resolveKuaishouTagEvent(bid.getTagId(), payment.getAct());
|
|
|
|
|
+ if (tagEvent == null) {
|
|
|
|
|
+ log.warn("[ConversionSync] 快手 eventType 未配置 | qk={} | tagId={} | act={}",
|
|
|
|
|
+ payment.getQk(), bid.getTagId(), payment.getAct());
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return CallbackTask.kuaishou(
|
|
|
|
|
+ payment,
|
|
|
|
|
+ bid,
|
|
|
|
|
+ kuaishouCallbackDedupeKey(payment, bid, tagEvent.getEventType()),
|
|
|
|
|
+ tagEvent
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private Integer resolveHonorConversionIdFromDb(String tagId, int baiduAct) {
|
|
private Integer resolveHonorConversionIdFromDb(String tagId, int baiduAct) {
|
|
|
if (honorColdStore == null) {
|
|
if (honorColdStore == null) {
|
|
|
return null;
|
|
return null;
|
|
@@ -363,6 +426,23 @@ public class ConversionSyncService {
|
|
|
return fallback != null ? fallback.getConversionId() : null;
|
|
return fallback != null ? fallback.getConversionId() : null;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private KuaishouTagEventRecord resolveKuaishouTagEvent(String tagId, int baiduAct) {
|
|
|
|
|
+ if (tagId == null || tagId.isBlank() || kuaishouColdStore == null) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ KuaishouTagEventRecord record = kuaishouColdStore.getTagEvent(tagId, baiduAct);
|
|
|
|
|
+ if (record == null) {
|
|
|
|
|
+ record = kuaishouColdStore.getTagEvent(tagId, 0);
|
|
|
|
|
+ }
|
|
|
|
|
+ return record;
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.warn("[ConversionSync] 查询快手 tag event 失败 | tagId={} | act={} | error={}",
|
|
|
|
|
+ tagId, baiduAct, e.getMessage(), e);
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// ─── Step 3: 统一落百度转化明细 ───────────────────────────────────────────
|
|
// ─── Step 3: 统一落百度转化明细 ───────────────────────────────────────────
|
|
|
|
|
|
|
|
private void saveConversions(List<ConversionRecord> records) {
|
|
private void saveConversions(List<ConversionRecord> records) {
|
|
@@ -382,6 +462,7 @@ public class ConversionSyncService {
|
|
|
Set<String> existing = new HashSet<>();
|
|
Set<String> existing = new HashSet<>();
|
|
|
existing.addAll(collectTencentSuccessfulCallbackKeys(callbackTasks));
|
|
existing.addAll(collectTencentSuccessfulCallbackKeys(callbackTasks));
|
|
|
existing.addAll(collectHonorSuccessfulCallbackKeys(callbackTasks));
|
|
existing.addAll(collectHonorSuccessfulCallbackKeys(callbackTasks));
|
|
|
|
|
+ existing.addAll(collectKuaishouSuccessfulCallbackKeys(callbackTasks));
|
|
|
|
|
|
|
|
List<CallbackTask> pending = new ArrayList<>(callbackTasks.size());
|
|
List<CallbackTask> pending = new ArrayList<>(callbackTasks.size());
|
|
|
for (CallbackTask task : callbackTasks) {
|
|
for (CallbackTask task : callbackTasks) {
|
|
@@ -447,6 +528,30 @@ public class ConversionSyncService {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private Set<String> collectKuaishouSuccessfulCallbackKeys(List<CallbackTask> callbackTasks) {
|
|
|
|
|
+ if (kuaishouColdStore == null) {
|
|
|
|
|
+ return Set.of();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Set<String> keys = new HashSet<>();
|
|
|
|
|
+ for (CallbackTask task : callbackTasks) {
|
|
|
|
|
+ if (task.isKuaishou()) {
|
|
|
|
|
+ keys.add(task.callbackKey);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (keys.isEmpty()) {
|
|
|
|
|
+ return Set.of();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ return kuaishouColdStore.successfulCallbackKeys(keys);
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("[ConversionSync] kuaishou dedup check error: size={}, {}", keys.size(), e.getMessage(), e);
|
|
|
|
|
+ return Set.of();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// ─── Step 5: 并发回传媒体 ───────────────────────────────────────────────
|
|
// ─── Step 5: 并发回传媒体 ───────────────────────────────────────────────
|
|
|
|
|
|
|
|
private List<CallbackDispatchResult> dispatchCallbacks(List<CallbackTask> tasks,
|
|
private List<CallbackDispatchResult> dispatchCallbacks(List<CallbackTask> tasks,
|
|
@@ -496,6 +601,17 @@ public class ConversionSyncService {
|
|
|
return CallbackDispatchResult.honor(task, toCallbackRecord(task, honorRecord));
|
|
return CallbackDispatchResult.honor(task, toCallbackRecord(task, honorRecord));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ if (task.isKuaishou()) {
|
|
|
|
|
+ int deductionRate = resolveKuaishouDeductionRate(task.kuaishouBid);
|
|
|
|
|
+ if (handleKuaishouDeduction(task)) {
|
|
|
|
|
+ return CallbackDispatchResult.kuaishou(task, toCallbackRecord(task, buildDeductedKuaishouCallbackRecord(task, deductionRate)));
|
|
|
|
|
+ }
|
|
|
|
|
+ KuaishouMediaCallbackRecord kuaishouRecord = kuaishouClient.sendConversion(
|
|
|
|
|
+ task.kuaishouBid, task.payment, task.kuaishouTagEvent, true);
|
|
|
|
|
+ kuaishouRecord.setDeductionRate(deductionRate);
|
|
|
|
|
+ return CallbackDispatchResult.kuaishou(task, toCallbackRecord(task, kuaishouRecord));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
return null;
|
|
return null;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -508,9 +624,12 @@ public class ConversionSyncService {
|
|
|
|
|
|
|
|
List<MediaCallbackRecord> tencentRecords = new ArrayList<>();
|
|
List<MediaCallbackRecord> tencentRecords = new ArrayList<>();
|
|
|
List<HonorMediaCallbackRecord> honorRecords = new ArrayList<>();
|
|
List<HonorMediaCallbackRecord> honorRecords = new ArrayList<>();
|
|
|
|
|
+ List<KuaishouMediaCallbackRecord> kuaishouRecords = new ArrayList<>();
|
|
|
for (CallbackDispatchResult result : results) {
|
|
for (CallbackDispatchResult result : results) {
|
|
|
if (result.isHonor()) {
|
|
if (result.isHonor()) {
|
|
|
honorRecords.add(toHonorCallbackRecord(result.task, result.record));
|
|
honorRecords.add(toHonorCallbackRecord(result.task, result.record));
|
|
|
|
|
+ } else if (result.isKuaishou()) {
|
|
|
|
|
+ kuaishouRecords.add(toKuaishouCallbackRecord(result.task, result.record));
|
|
|
} else {
|
|
} else {
|
|
|
tencentRecords.add(result.record);
|
|
tencentRecords.add(result.record);
|
|
|
}
|
|
}
|
|
@@ -531,6 +650,14 @@ public class ConversionSyncService {
|
|
|
log.error("[ConversionSync] batch save honor callbacks failed: size={}, {}", honorRecords.size(), e.getMessage(), e);
|
|
log.error("[ConversionSync] batch save honor callbacks failed: size={}, {}", honorRecords.size(), e.getMessage(), e);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ if (kuaishouColdStore != null && !kuaishouRecords.isEmpty()) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ kuaishouColdStore.saveMediaCallbacks(kuaishouRecords);
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("[ConversionSync] batch save kuaishou callbacks failed: size={}, {}", kuaishouRecords.size(), e.getMessage(), e);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// ─── 工具方法 ────────────────────────────────────────────────────────────
|
|
// ─── 工具方法 ────────────────────────────────────────────────────────────
|
|
@@ -593,6 +720,26 @@ public class ConversionSyncService {
|
|
|
return record;
|
|
return record;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private static ConversionRecord toKuaishouConversionRecord(PaymentInfo payment, KuaishouBidRecord bid) {
|
|
|
|
|
+ ConversionRecord record = new ConversionRecord();
|
|
|
|
|
+ record.setDedupeKey(conversionDedupeKey(payment));
|
|
|
|
|
+ record.setQk(payment.getQk());
|
|
|
|
|
+ record.setMedia("kuaishou");
|
|
|
|
|
+ record.setTagId(bid != null ? bid.getTagId() : null);
|
|
|
|
|
+ record.setDate(payment.getDate());
|
|
|
|
|
+ record.setAppSid(payment.getAppSid());
|
|
|
|
|
+ record.setCustomerName(payment.getCustomerName());
|
|
|
|
|
+ record.setDeviceId(payment.getDeviceId());
|
|
|
|
|
+ record.setConv(payment.getConv());
|
|
|
|
|
+ record.setPayment(payment.getPayment());
|
|
|
|
|
+ record.setGmv(payment.getGmv());
|
|
|
|
|
+ record.setAct(payment.getAct());
|
|
|
|
|
+ record.setTu(payment.getTu());
|
|
|
|
|
+ record.setClkTime(payment.getClkTime());
|
|
|
|
|
+ record.setCreatedAt(Instant.now());
|
|
|
|
|
+ return record;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
static String conversionDedupeKey(PaymentInfo payment) {
|
|
static String conversionDedupeKey(PaymentInfo payment) {
|
|
|
return String.format("%s:%s:%s:%d",
|
|
return String.format("%s:%s:%s:%d",
|
|
|
payment.getQk(),
|
|
payment.getQk(),
|
|
@@ -632,6 +779,16 @@ public class ConversionSyncService {
|
|
|
conversionId);
|
|
conversionId);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ static String kuaishouCallbackDedupeKey(PaymentInfo payment, KuaishouBidRecord bid, int eventType) {
|
|
|
|
|
+ return String.format("kuaishou:%s:%s:%s:%s:%d:%d",
|
|
|
|
|
+ payment.getQk(),
|
|
|
|
|
+ payment.getDate(),
|
|
|
|
|
+ payment.getDeviceId(),
|
|
|
|
|
+ bid.getTagId(),
|
|
|
|
|
+ payment.getAct(),
|
|
|
|
|
+ eventType);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private DeductionHandling handleTencentDeduction(CallbackTask task) {
|
|
private DeductionHandling handleTencentDeduction(CallbackTask task) {
|
|
|
if (!task.isTencent() || task.tencentBid == null) {
|
|
if (!task.isTencent() || task.tencentBid == null) {
|
|
|
return DeductionHandling.SEND;
|
|
return DeductionHandling.SEND;
|
|
@@ -704,6 +861,60 @@ public class ConversionSyncService {
|
|
|
return record;
|
|
return record;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private boolean handleKuaishouDeduction(CallbackTask task) {
|
|
|
|
|
+ if (!task.isKuaishou() || task.kuaishouBid == null || kuaishouHotStore == null) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ int deductionRate = resolveKuaishouDeductionRate(task.kuaishouBid);
|
|
|
|
|
+ if (task.payment.getAct() == 2001 || deductionRate <= 0) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!kuaishouHotStore.markConversionSeen(task.callbackKey)) {
|
|
|
|
|
+ log.info("[ConversionSync] 快手转化已见过,跳过重复扣量判断 | callbackKey={} | qk={} | accountId={}",
|
|
|
|
|
+ task.callbackKey, task.payment.getQk(), safe(task.kuaishouBid.getAccountId()));
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ KuaishouHotStore.DeductionDecision decision = kuaishouHotStore.evaluateDeduction(
|
|
|
|
|
+ task.kuaishouBid.getAccountId(),
|
|
|
|
|
+ task.payment.getAct(),
|
|
|
|
|
+ task.payment.getDate(),
|
|
|
|
|
+ deductionRate
|
|
|
|
|
+ );
|
|
|
|
|
+ boolean deduct = decision.isShouldDeduct();
|
|
|
|
|
+ log.info("[ConversionSync] 快手转化扣量判定 | qk={} | act={} | accountId={} | tagId={} | rate={} | totalSeen={} | totalDeducted={} | callbackKey={} | deduct={}",
|
|
|
|
|
+ task.payment.getQk(), task.payment.getAct(), safe(task.kuaishouBid.getAccountId()),
|
|
|
|
|
+ safe(task.kuaishouBid.getTagId()), deductionRate,
|
|
|
|
|
+ decision.getTotalSeen(), decision.getTotalDeducted(), task.callbackKey, deduct);
|
|
|
|
|
+ return deduct;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private KuaishouMediaCallbackRecord buildDeductedKuaishouCallbackRecord(CallbackTask task, int deductionRate) {
|
|
|
|
|
+ KuaishouMediaCallbackRecord record = new KuaishouMediaCallbackRecord();
|
|
|
|
|
+ Instant now = Instant.now();
|
|
|
|
|
+ record.setDedupeKey(task.callbackKey);
|
|
|
|
|
+ record.setQk(task.payment.getQk());
|
|
|
|
|
+ record.setTagId(task.kuaishouBid.getTagId());
|
|
|
|
|
+ String callbackUrl = task.kuaishouBid.getMediaParams() != null
|
|
|
|
|
+ ? task.kuaishouBid.getMediaParams().get(KuaishouBidRecord.CALLBACK_KEY)
|
|
|
|
|
+ : null;
|
|
|
|
|
+ record.setCallbackUrl(callbackUrl != null ? callbackUrl : "");
|
|
|
|
|
+ record.setEventType(task.kuaishouTagEvent.getEventType());
|
|
|
|
|
+ record.setEventTimeMs(now.toEpochMilli());
|
|
|
|
|
+ record.setPurchase(Math.max(task.payment.getPayment(), 0D));
|
|
|
|
|
+ record.setDeductionRate(deductionRate);
|
|
|
|
|
+ record.setDirectMatch(false);
|
|
|
|
|
+ record.setDispatchStatus(KuaishouMediaCallbackRecord.DISPATCH_STATUS_DEDUCTED);
|
|
|
|
|
+ record.setStatus(0);
|
|
|
|
|
+ record.setOk(false);
|
|
|
|
|
+ record.setAttempt(1);
|
|
|
|
|
+ record.setErrorMessage("deducted_by_rate");
|
|
|
|
|
+ record.setCreatedAt(now);
|
|
|
|
|
+ log.info("[ConversionSync] 快手转化扣量 | qk={} | act={} | rate={} | tagId={} | accountId={} | callbackKey={}",
|
|
|
|
|
+ task.payment.getQk(), task.payment.getAct(), deductionRate,
|
|
|
|
|
+ task.kuaishouBid.getTagId(), safe(task.kuaishouBid.getAccountId()), task.callbackKey);
|
|
|
|
|
+ return record;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private static String resolveTrackingVersion(BidRecord bid) {
|
|
private static String resolveTrackingVersion(BidRecord bid) {
|
|
|
if (bid == null || bid.getMediaParams() == null) {
|
|
if (bid == null || bid.getMediaParams() == null) {
|
|
|
return "v1";
|
|
return "v1";
|
|
@@ -726,6 +937,20 @@ public class ConversionSyncService {
|
|
|
return 0;
|
|
return 0;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private int resolveKuaishouDeductionRate(KuaishouBidRecord bid) {
|
|
|
|
|
+ if (bid == null || bid.getMediaParams() == null) {
|
|
|
|
|
+ return 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ String rawRate = bid.getMediaParams().get(KuaishouBidRecord.DEDUCTION_RATE_KEY);
|
|
|
|
|
+ if (rawRate != null && !rawRate.isBlank()) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ return sanitizeDeductionRate(Integer.parseInt(rawRate.trim()));
|
|
|
|
|
+ } catch (NumberFormatException ignored) {
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private static String safe(String value) {
|
|
private static String safe(String value) {
|
|
|
return value == null ? "" : value;
|
|
return value == null ? "" : value;
|
|
|
}
|
|
}
|
|
@@ -756,6 +981,28 @@ public class ConversionSyncService {
|
|
|
return callback;
|
|
return callback;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private static KuaishouMediaCallbackRecord toKuaishouCallbackRecord(CallbackTask task, MediaCallbackRecord record) {
|
|
|
|
|
+ KuaishouMediaCallbackRecord callback = new KuaishouMediaCallbackRecord();
|
|
|
|
|
+ callback.setDedupeKey(record.getDedupeKey());
|
|
|
|
|
+ callback.setQk(record.getQk());
|
|
|
|
|
+ callback.setTagId(task.tagId);
|
|
|
|
|
+ callback.setCallbackUrl(record.getCallbackUrl());
|
|
|
|
|
+ callback.setEventType(record.getEventType());
|
|
|
|
|
+ callback.setEventTimeMs(record.getEventTimeMs());
|
|
|
|
|
+ callback.setPurchase(record.getPurchase());
|
|
|
|
|
+ callback.setDeductionRate(parseKuaishouDeductionRate(task.kuaishouBid));
|
|
|
|
|
+ callback.setStatus(record.getStatus());
|
|
|
|
|
+ callback.setOk(record.isOk());
|
|
|
|
|
+ callback.setAttempt(record.getAttempt());
|
|
|
|
|
+ callback.setResponseBody(record.getResponseBody());
|
|
|
|
|
+ callback.setErrorMessage(record.getErrorMessage());
|
|
|
|
|
+ callback.setRequestBody(record.getRequestBody());
|
|
|
|
|
+ callback.setDispatchStatus(record.getDispatchStatus());
|
|
|
|
|
+ callback.setDirectMatch(!KuaishouMediaCallbackRecord.DISPATCH_STATUS_DEDUCTED.equals(record.getDispatchStatus()));
|
|
|
|
|
+ callback.setCreatedAt(record.getCreatedAt());
|
|
|
|
|
+ return callback;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private static MediaCallbackRecord toCallbackRecord(CallbackTask task, HonorMediaCallbackRecord honorRecord) {
|
|
private static MediaCallbackRecord toCallbackRecord(CallbackTask task, HonorMediaCallbackRecord honorRecord) {
|
|
|
MediaCallbackRecord callbackRecord = new MediaCallbackRecord();
|
|
MediaCallbackRecord callbackRecord = new MediaCallbackRecord();
|
|
|
callbackRecord.setDedupeKey(task.callbackKey);
|
|
callbackRecord.setDedupeKey(task.callbackKey);
|
|
@@ -776,15 +1023,59 @@ public class ConversionSyncService {
|
|
|
return callbackRecord;
|
|
return callbackRecord;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private static MediaCallbackRecord toCallbackRecord(CallbackTask task, KuaishouMediaCallbackRecord kuaishouRecord) {
|
|
|
|
|
+ MediaCallbackRecord callbackRecord = new MediaCallbackRecord();
|
|
|
|
|
+ callbackRecord.setDedupeKey(task.callbackKey);
|
|
|
|
|
+ callbackRecord.setMedia("kuaishou");
|
|
|
|
|
+ callbackRecord.setQk(kuaishouRecord.getQk());
|
|
|
|
|
+ callbackRecord.setCallbackUrl(kuaishouRecord.getCallbackUrl());
|
|
|
|
|
+ callbackRecord.setEventType(kuaishouRecord.getEventType());
|
|
|
|
|
+ callbackRecord.setEventTimeMs(kuaishouRecord.getEventTimeMs());
|
|
|
|
|
+ callbackRecord.setPurchase(kuaishouRecord.getPurchase());
|
|
|
|
|
+ callbackRecord.setStatus(kuaishouRecord.getStatus());
|
|
|
|
|
+ callbackRecord.setOk(kuaishouRecord.isOk());
|
|
|
|
|
+ callbackRecord.setAttempt(kuaishouRecord.getAttempt());
|
|
|
|
|
+ callbackRecord.setResponseBody(kuaishouRecord.getResponseBody());
|
|
|
|
|
+ callbackRecord.setErrorMessage(kuaishouRecord.getErrorMessage());
|
|
|
|
|
+ callbackRecord.setRequestBody(kuaishouRecord.getRequestBody());
|
|
|
|
|
+ callbackRecord.setDispatchStatus(kuaishouRecord.getDispatchStatus());
|
|
|
|
|
+ callbackRecord.setTrackingVersion(kuaishouRecord.isDirectMatch() ? "direct" : "deducted");
|
|
|
|
|
+ callbackRecord.setCreatedAt(kuaishouRecord.getCreatedAt());
|
|
|
|
|
+ return callbackRecord;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static int parseKuaishouDeductionRate(KuaishouBidRecord bid) {
|
|
|
|
|
+ if (bid == null || bid.getMediaParams() == null) {
|
|
|
|
|
+ return 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ String rawRate = bid.getMediaParams().get(KuaishouBidRecord.DEDUCTION_RATE_KEY);
|
|
|
|
|
+ if (rawRate == null || rawRate.isBlank()) {
|
|
|
|
|
+ return 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ int rate = Integer.parseInt(rawRate.trim());
|
|
|
|
|
+ if (rate < 0) {
|
|
|
|
|
+ return 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ return Math.min(rate, 100);
|
|
|
|
|
+ } catch (NumberFormatException ignored) {
|
|
|
|
|
+ return 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// ─── 内部数据结构 ────────────────────────────────────────────────────────
|
|
// ─── 内部数据结构 ────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
private static class BidLookup {
|
|
private static class BidLookup {
|
|
|
private final Map<String, BidRecord> tencentBids;
|
|
private final Map<String, BidRecord> tencentBids;
|
|
|
private final Map<String, HonorBidRecord> honorBids;
|
|
private final Map<String, HonorBidRecord> honorBids;
|
|
|
|
|
+ private final Map<String, KuaishouBidRecord> kuaishouBids;
|
|
|
|
|
|
|
|
- private BidLookup(Map<String, BidRecord> tencentBids, Map<String, HonorBidRecord> honorBids) {
|
|
|
|
|
|
|
+ private BidLookup(Map<String, BidRecord> tencentBids,
|
|
|
|
|
+ Map<String, HonorBidRecord> honorBids,
|
|
|
|
|
+ Map<String, KuaishouBidRecord> kuaishouBids) {
|
|
|
this.tencentBids = tencentBids;
|
|
this.tencentBids = tencentBids;
|
|
|
this.honorBids = honorBids;
|
|
this.honorBids = honorBids;
|
|
|
|
|
+ this.kuaishouBids = kuaishouBids;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -797,6 +1088,8 @@ public class ConversionSyncService {
|
|
|
private final String tencentActionType;
|
|
private final String tencentActionType;
|
|
|
private final HonorBidRecord honorBid;
|
|
private final HonorBidRecord honorBid;
|
|
|
private final Integer honorConversionId;
|
|
private final Integer honorConversionId;
|
|
|
|
|
+ private final KuaishouBidRecord kuaishouBid;
|
|
|
|
|
+ private final KuaishouTagEventRecord kuaishouTagEvent;
|
|
|
|
|
|
|
|
private CallbackTask(String media,
|
|
private CallbackTask(String media,
|
|
|
PaymentInfo payment,
|
|
PaymentInfo payment,
|
|
@@ -805,7 +1098,9 @@ public class ConversionSyncService {
|
|
|
BidRecord tencentBid,
|
|
BidRecord tencentBid,
|
|
|
String tencentActionType,
|
|
String tencentActionType,
|
|
|
HonorBidRecord honorBid,
|
|
HonorBidRecord honorBid,
|
|
|
- Integer honorConversionId) {
|
|
|
|
|
|
|
+ Integer honorConversionId,
|
|
|
|
|
+ KuaishouBidRecord kuaishouBid,
|
|
|
|
|
+ KuaishouTagEventRecord kuaishouTagEvent) {
|
|
|
this.media = media;
|
|
this.media = media;
|
|
|
this.payment = payment;
|
|
this.payment = payment;
|
|
|
this.tagId = tagId;
|
|
this.tagId = tagId;
|
|
@@ -814,20 +1109,29 @@ public class ConversionSyncService {
|
|
|
this.tencentActionType = tencentActionType;
|
|
this.tencentActionType = tencentActionType;
|
|
|
this.honorBid = honorBid;
|
|
this.honorBid = honorBid;
|
|
|
this.honorConversionId = honorConversionId;
|
|
this.honorConversionId = honorConversionId;
|
|
|
|
|
+ this.kuaishouBid = kuaishouBid;
|
|
|
|
|
+ this.kuaishouTagEvent = kuaishouTagEvent;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private static CallbackTask tencent(PaymentInfo payment,
|
|
private static CallbackTask tencent(PaymentInfo payment,
|
|
|
BidRecord bid,
|
|
BidRecord bid,
|
|
|
String callbackKey,
|
|
String callbackKey,
|
|
|
String actionType) {
|
|
String actionType) {
|
|
|
- return new CallbackTask("tencent", payment, bid.getTagId(), callbackKey, bid, actionType, null, null);
|
|
|
|
|
|
|
+ return new CallbackTask("tencent", payment, bid.getTagId(), callbackKey, bid, actionType, null, null, null, null);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private static CallbackTask honor(PaymentInfo payment,
|
|
private static CallbackTask honor(PaymentInfo payment,
|
|
|
HonorBidRecord bid,
|
|
HonorBidRecord bid,
|
|
|
String callbackKey,
|
|
String callbackKey,
|
|
|
Integer conversionId) {
|
|
Integer conversionId) {
|
|
|
- return new CallbackTask("honor", payment, bid.getTagId(), callbackKey, null, null, bid, conversionId);
|
|
|
|
|
|
|
+ return new CallbackTask("honor", payment, bid.getTagId(), callbackKey, null, null, bid, conversionId, null, null);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static CallbackTask kuaishou(PaymentInfo payment,
|
|
|
|
|
+ KuaishouBidRecord bid,
|
|
|
|
|
+ String callbackKey,
|
|
|
|
|
+ KuaishouTagEventRecord tagEvent) {
|
|
|
|
|
+ return new CallbackTask("kuaishou", payment, bid.getTagId(), callbackKey, null, null, null, null, bid, tagEvent);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private boolean isTencent() {
|
|
private boolean isTencent() {
|
|
@@ -837,6 +1141,10 @@ public class ConversionSyncService {
|
|
|
private boolean isHonor() {
|
|
private boolean isHonor() {
|
|
|
return "honor".equals(media);
|
|
return "honor".equals(media);
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ private boolean isKuaishou() {
|
|
|
|
|
+ return "kuaishou".equals(media);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private static class CallbackDispatchResult {
|
|
private static class CallbackDispatchResult {
|
|
@@ -858,9 +1166,17 @@ public class ConversionSyncService {
|
|
|
return new CallbackDispatchResult("honor", task, record);
|
|
return new CallbackDispatchResult("honor", task, record);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private static CallbackDispatchResult kuaishou(CallbackTask task, MediaCallbackRecord record) {
|
|
|
|
|
+ return new CallbackDispatchResult("kuaishou", task, record);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private boolean isHonor() {
|
|
private boolean isHonor() {
|
|
|
return "honor".equals(media);
|
|
return "honor".equals(media);
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ private boolean isKuaishou() {
|
|
|
|
|
+ return "kuaishou".equals(media);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private enum DeductionHandling {
|
|
private enum DeductionHandling {
|