|
|
@@ -25,6 +25,13 @@ import com.adx.tencent.storage.model.ConversionRecord;
|
|
|
import com.adx.tencent.storage.model.MediaCallbackRecord;
|
|
|
import com.adx.tencent.tagsync.TagEventResolver;
|
|
|
import com.adx.tencent.tencent.TencentClient;
|
|
|
+import com.adx.tencent.vivo.client.VivoClient;
|
|
|
+import com.adx.tencent.vivo.model.VivoBidRecord;
|
|
|
+import com.adx.tencent.vivo.model.VivoMediaCallbackRecord;
|
|
|
+import com.adx.tencent.vivo.model.VivoTagEventRecord;
|
|
|
+import com.adx.tencent.vivo.service.VivoTagEventResolver;
|
|
|
+import com.adx.tencent.vivo.store.VivoColdStore;
|
|
|
+import com.adx.tencent.vivo.store.VivoHotStore;
|
|
|
import org.slf4j.Logger;
|
|
|
import org.slf4j.LoggerFactory;
|
|
|
|
|
|
@@ -61,20 +68,32 @@ public class ConversionSyncService {
|
|
|
private static final int KS_EVENT_APP_WAKEUP = 84;
|
|
|
private static final int KS_EVENT_KEY_ACTION = 143;
|
|
|
private static final int KS_EVENT_THREE_DAY_RETENTION = 103;
|
|
|
+ private static final String VIVO_EVENT_ACTIVATION = "ACTIVATION";
|
|
|
+ private static final String VIVO_EVENT_REGISTER = "REGISTER";
|
|
|
+ private static final String VIVO_EVENT_PAY = "PAY";
|
|
|
+ private static final String VIVO_EVENT_RETENTION_1 = "RETENTION_1";
|
|
|
+ private static final String VIVO_EVENT_RETENTION_3 = "RETENTION_3";
|
|
|
+ private static final String VIVO_EVENT_RETENTION_7 = "RETENTION_7";
|
|
|
+ private static final String VIVO_EVENT_REACTIVATION = "REACTIVATION";
|
|
|
+ private static final String VIVO_EVENT_OTHER = "OTHER";
|
|
|
|
|
|
private final ConversionClient baiduClient;
|
|
|
private final RedisHotStore hotStore;
|
|
|
private final HonorHotStore honorHotStore;
|
|
|
private final KuaishouHotStore kuaishouHotStore;
|
|
|
+ private final VivoHotStore vivoHotStore;
|
|
|
private final TencentClient tencentClient;
|
|
|
private final TiDBColdStore coldStore;
|
|
|
private final HonorColdStore honorColdStore;
|
|
|
private final KuaishouColdStore kuaishouColdStore;
|
|
|
+ private final VivoColdStore vivoColdStore;
|
|
|
private final HonorClient honorClient;
|
|
|
private final KuaishouClient kuaishouClient;
|
|
|
+ private final VivoClient vivoClient;
|
|
|
private final TagEventResolver tagEventResolver;
|
|
|
private final HonorTagEventResolver honorTagEventResolver;
|
|
|
private final KuaishouTagEventResolver kuaishouTagEventResolver;
|
|
|
+ private final VivoTagEventResolver vivoTagEventResolver;
|
|
|
private final ExecutorService realtimeExecutor;
|
|
|
private final ExecutorService backfillExecutor;
|
|
|
|
|
|
@@ -90,12 +109,16 @@ public class ConversionSyncService {
|
|
|
TagEventResolver tagEventResolver,
|
|
|
HonorTagEventResolver honorTagEventResolver,
|
|
|
KuaishouTagEventResolver kuaishouTagEventResolver,
|
|
|
+ VivoTagEventResolver vivoTagEventResolver,
|
|
|
HonorHotStore honorHotStore,
|
|
|
HonorColdStore honorColdStore,
|
|
|
KuaishouHotStore kuaishouHotStore,
|
|
|
KuaishouColdStore kuaishouColdStore,
|
|
|
+ VivoHotStore vivoHotStore,
|
|
|
+ VivoColdStore vivoColdStore,
|
|
|
HonorClient honorClient,
|
|
|
KuaishouClient kuaishouClient,
|
|
|
+ VivoClient vivoClient,
|
|
|
AppProperties props) {
|
|
|
this.baiduClient = baiduClient;
|
|
|
this.hotStore = hotStore;
|
|
|
@@ -104,12 +127,16 @@ public class ConversionSyncService {
|
|
|
this.tagEventResolver = tagEventResolver;
|
|
|
this.honorTagEventResolver = honorTagEventResolver;
|
|
|
this.kuaishouTagEventResolver = kuaishouTagEventResolver;
|
|
|
+ this.vivoTagEventResolver = vivoTagEventResolver;
|
|
|
this.honorHotStore = honorHotStore;
|
|
|
this.honorColdStore = honorColdStore;
|
|
|
this.kuaishouHotStore = kuaishouHotStore;
|
|
|
this.kuaishouColdStore = kuaishouColdStore;
|
|
|
+ this.vivoHotStore = vivoHotStore;
|
|
|
+ this.vivoColdStore = vivoColdStore;
|
|
|
this.honorClient = honorClient;
|
|
|
this.kuaishouClient = kuaishouClient;
|
|
|
+ this.vivoClient = vivoClient;
|
|
|
this.realtimeExecutor = Executors.newFixedThreadPool(
|
|
|
REALTIME_CONCURRENCY,
|
|
|
r -> {
|
|
|
@@ -221,7 +248,10 @@ public class ConversionSyncService {
|
|
|
Map<String, KuaishouBidRecord> kuaishouBids = kuaishouHotStore != null
|
|
|
? kuaishouHotStore.findBidsByQks(qkList)
|
|
|
: Map.of();
|
|
|
- return new BidLookup(tencentBids, honorBids, kuaishouBids);
|
|
|
+ Map<String, VivoBidRecord> vivoBids = vivoHotStore != null
|
|
|
+ ? vivoHotStore.findBidsByQks(qkList)
|
|
|
+ : Map.of();
|
|
|
+ return new BidLookup(tencentBids, honorBids, kuaishouBids, vivoBids);
|
|
|
}
|
|
|
|
|
|
private void backfillTencentAccountIds(List<String> qkList, Map<String, BidRecord> tencentBids) {
|
|
|
@@ -276,8 +306,9 @@ public class ConversionSyncService {
|
|
|
BidRecord tencentBid = lookup.tencentBids.get(payment.getQk());
|
|
|
HonorBidRecord honorBid = lookup.honorBids.get(payment.getQk());
|
|
|
KuaishouBidRecord kuaishouBid = lookup.kuaishouBids.get(payment.getQk());
|
|
|
+ VivoBidRecord vivoBid = lookup.vivoBids.get(payment.getQk());
|
|
|
|
|
|
- if (tencentBid == null && honorBid == null && kuaishouBid == null) {
|
|
|
+ if (tencentBid == null && honorBid == null && kuaishouBid == null && vivoBid == null) {
|
|
|
conversionRecords.add(toConversionRecord(payment, null, ""));
|
|
|
result.skipped.incrementAndGet();
|
|
|
continue;
|
|
|
@@ -286,9 +317,11 @@ public class ConversionSyncService {
|
|
|
boolean hasTencentBid = tencentBid != null;
|
|
|
boolean hasHonorBid = honorBid != null;
|
|
|
boolean hasKuaishouBid = kuaishouBid != null;
|
|
|
+ boolean hasVivoBid = vivoBid != null;
|
|
|
boolean validTencent = hasTencentBid && "tencent".equals(tencentBid.getMedia());
|
|
|
boolean validHonor = hasHonorBid && "honor".equals(honorBid.getMedia());
|
|
|
boolean validKuaishou = hasKuaishouBid && "kuaishou".equals(kuaishouBid.getMedia());
|
|
|
+ boolean validVivo = hasVivoBid && "vivo".equals(vivoBid.getMedia());
|
|
|
|
|
|
if (hasTencentBid && !validTencent) {
|
|
|
log.warn("[ConversionSync] 腾讯 bid media 非法 | qk={} | media={}", payment.getQk(), tencentBid.getMedia());
|
|
|
@@ -302,7 +335,11 @@ public class ConversionSyncService {
|
|
|
log.warn("[ConversionSync] 快手 bid media 非法 | qk={} | media={}", payment.getQk(), kuaishouBid.getMedia());
|
|
|
}
|
|
|
|
|
|
- int matchedMediaCount = (validTencent ? 1 : 0) + (validHonor ? 1 : 0) + (validKuaishou ? 1 : 0);
|
|
|
+ if (hasVivoBid && !validVivo) {
|
|
|
+ log.warn("[ConversionSync] vivo bid media 非法 | qk={} | media={}", payment.getQk(), vivoBid.getMedia());
|
|
|
+ }
|
|
|
+
|
|
|
+ int matchedMediaCount = (validTencent ? 1 : 0) + (validHonor ? 1 : 0) + (validKuaishou ? 1 : 0) + (validVivo ? 1 : 0);
|
|
|
if (matchedMediaCount > 1) {
|
|
|
log.warn("[ConversionSync] qk 同时命中多媒体,跳过分发 | qk={} | act={} | date={}",
|
|
|
payment.getQk(), payment.getAct(), payment.getDate());
|
|
|
@@ -323,34 +360,47 @@ public class ConversionSyncService {
|
|
|
continue;
|
|
|
}
|
|
|
|
|
|
- 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());
|
|
|
+ if (validHonor) {
|
|
|
+ conversionRecords.add(toHonorConversionRecord(payment, honorBid));
|
|
|
+ result.matched.incrementAndGet();
|
|
|
+ log.info("[ConversionSync] 匹配到荣耀转化 | qk={} | act={} | deviceId={} | date={} | payment={}",
|
|
|
+ payment.getQk(), payment.getAct(), payment.getDeviceId(), payment.getDate(), payment.getPayment());
|
|
|
|
|
|
- List<CallbackTask> kuaishouTasks = buildKuaishouCallbackTasks(payment, kuaishouBid);
|
|
|
- if (!kuaishouTasks.isEmpty()) {
|
|
|
- callbackTasks.addAll(kuaishouTasks);
|
|
|
- }
|
|
|
- continue;
|
|
|
+ CallbackTask honorTask = buildHonorCallbackTask(payment, honorBid);
|
|
|
+ if (honorTask != null) {
|
|
|
+ callbackTasks.add(honorTask);
|
|
|
}
|
|
|
+ continue;
|
|
|
+ }
|
|
|
|
|
|
- conversionRecords.add(toConversionRecord(payment, hasTencentBid ? tencentBid : null, hasTencentBid ? tencentBid.getMedia() : ""));
|
|
|
- result.skipped.incrementAndGet();
|
|
|
+ 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());
|
|
|
+
|
|
|
+ List<CallbackTask> kuaishouTasks = buildKuaishouCallbackTasks(payment, kuaishouBid);
|
|
|
+ if (!kuaishouTasks.isEmpty()) {
|
|
|
+ callbackTasks.addAll(kuaishouTasks);
|
|
|
+ }
|
|
|
continue;
|
|
|
}
|
|
|
|
|
|
- conversionRecords.add(toHonorConversionRecord(payment, honorBid));
|
|
|
- result.matched.incrementAndGet();
|
|
|
- log.info("[ConversionSync] 匹配到荣耀转化 | qk={} | act={} | deviceId={} | date={} | payment={}",
|
|
|
- payment.getQk(), payment.getAct(), payment.getDeviceId(), payment.getDate(), payment.getPayment());
|
|
|
+ if (validVivo) {
|
|
|
+ conversionRecords.add(toVivoConversionRecord(payment, vivoBid));
|
|
|
+ result.matched.incrementAndGet();
|
|
|
+ log.info("[ConversionSync] 匹配到 vivo 转化 | qk={} | act={} | deviceId={} | date={} | payment={}",
|
|
|
+ payment.getQk(), payment.getAct(), payment.getDeviceId(), payment.getDate(), payment.getPayment());
|
|
|
|
|
|
- CallbackTask honorTask = buildHonorCallbackTask(payment, honorBid);
|
|
|
- if (honorTask != null) {
|
|
|
- callbackTasks.add(honorTask);
|
|
|
+ List<CallbackTask> vivoTasks = buildVivoCallbackTasks(payment, vivoBid);
|
|
|
+ if (!vivoTasks.isEmpty()) {
|
|
|
+ callbackTasks.addAll(vivoTasks);
|
|
|
+ }
|
|
|
+ continue;
|
|
|
}
|
|
|
+
|
|
|
+ conversionRecords.add(toConversionRecord(payment, hasTencentBid ? tencentBid : null, hasTencentBid ? tencentBid.getMedia() : ""));
|
|
|
+ result.skipped.incrementAndGet();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -426,6 +476,30 @@ public class ConversionSyncService {
|
|
|
return tasks;
|
|
|
}
|
|
|
|
|
|
+ private List<CallbackTask> buildVivoCallbackTasks(PaymentInfo payment, VivoBidRecord bid) {
|
|
|
+ if (vivoColdStore == null && vivoTagEventResolver == null) {
|
|
|
+ return List.of();
|
|
|
+ }
|
|
|
+
|
|
|
+ List<VivoTagEventRecord> tagEvents = resolveVivoTagEvents(bid.getTagId(), payment.getAct());
|
|
|
+ if (tagEvents.isEmpty()) {
|
|
|
+ log.warn("[ConversionSync] vivo eventType 未配置且无兜底映射 | qk={} | tagId={} | act={}",
|
|
|
+ payment.getQk(), bid.getTagId(), payment.getAct());
|
|
|
+ return List.of();
|
|
|
+ }
|
|
|
+
|
|
|
+ List<CallbackTask> tasks = new ArrayList<>(tagEvents.size());
|
|
|
+ for (VivoTagEventRecord tagEvent : tagEvents) {
|
|
|
+ tasks.add(CallbackTask.vivo(
|
|
|
+ payment,
|
|
|
+ bid,
|
|
|
+ vivoCallbackDedupeKey(payment, bid, tagEvent.getEventType()),
|
|
|
+ tagEvent
|
|
|
+ ));
|
|
|
+ }
|
|
|
+ return tasks;
|
|
|
+ }
|
|
|
+
|
|
|
private Integer resolveHonorConversionIdFromDb(String tagId, int baiduAct) {
|
|
|
if (honorColdStore == null) {
|
|
|
return null;
|
|
|
@@ -452,6 +526,20 @@ public class ConversionSyncService {
|
|
|
return fallback;
|
|
|
}
|
|
|
|
|
|
+ private List<VivoTagEventRecord> resolveVivoTagEvents(String tagId, int baiduAct) {
|
|
|
+ List<VivoTagEventRecord> configured = resolveConfiguredVivoTagEvents(tagId, baiduAct);
|
|
|
+ if (!configured.isEmpty()) {
|
|
|
+ return configured;
|
|
|
+ }
|
|
|
+
|
|
|
+ List<VivoTagEventRecord> fallback = buildVivoFallbackTagEvents(tagId, baiduAct);
|
|
|
+ if (!fallback.isEmpty()) {
|
|
|
+ log.warn("[ConversionSync] vivo eventType 未配置, 启用兜底映射 | tagId={} | act={} | fallbackEventTypes={}",
|
|
|
+ tagId, baiduAct, fallback.stream().map(VivoTagEventRecord::getEventType).toList());
|
|
|
+ }
|
|
|
+ return fallback;
|
|
|
+ }
|
|
|
+
|
|
|
private List<KuaishouTagEventRecord> resolveConfiguredKuaishouTagEvents(String tagId, int baiduAct) {
|
|
|
if (tagId == null || tagId.isBlank()) {
|
|
|
return List.of();
|
|
|
@@ -514,6 +602,67 @@ public class ConversionSyncService {
|
|
|
return records;
|
|
|
}
|
|
|
|
|
|
+ private List<VivoTagEventRecord> resolveConfiguredVivoTagEvents(String tagId, int baiduAct) {
|
|
|
+ if (tagId == null || tagId.isBlank()) {
|
|
|
+ return List.of();
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ if (vivoTagEventResolver != null) {
|
|
|
+ List<String> eventTypes = vivoTagEventResolver.resolveEventTypes(tagId, baiduAct);
|
|
|
+ if (eventTypes != null && !eventTypes.isEmpty()) {
|
|
|
+ List<VivoTagEventRecord> records = new ArrayList<>(eventTypes.size());
|
|
|
+ for (String eventType : eventTypes) {
|
|
|
+ records.add(buildVivoTagEvent(tagId, baiduAct, eventType));
|
|
|
+ }
|
|
|
+ return records;
|
|
|
+ }
|
|
|
+ return List.of();
|
|
|
+ }
|
|
|
+ if (vivoColdStore == null) {
|
|
|
+ return List.of();
|
|
|
+ }
|
|
|
+ List<VivoTagEventRecord> records = vivoColdStore.getTagEvents(tagId, baiduAct);
|
|
|
+ if (records == null || records.isEmpty()) {
|
|
|
+ records = vivoColdStore.getTagEvents(tagId, 0);
|
|
|
+ }
|
|
|
+ return records == null ? List.of() : records;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("[ConversionSync] 查询 vivo tag event 失败 | tagId={} | act={} | error={}",
|
|
|
+ tagId, baiduAct, e.getMessage(), e);
|
|
|
+ return List.of();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static VivoTagEventRecord buildVivoTagEvent(String tagId, int baiduAct, String eventType) {
|
|
|
+ VivoTagEventRecord record = new VivoTagEventRecord();
|
|
|
+ record.setTagId(tagId);
|
|
|
+ record.setBaiduAct(baiduAct);
|
|
|
+ record.setEventType(eventType);
|
|
|
+ return record;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static List<VivoTagEventRecord> buildVivoFallbackTagEvents(String tagId, int baiduAct) {
|
|
|
+ List<String> eventTypes = switch (baiduAct) {
|
|
|
+ case 1 -> List.of(VIVO_EVENT_ACTIVATION);
|
|
|
+ case 2, 5 -> List.of(VIVO_EVENT_PAY);
|
|
|
+ case 3 -> List.of(VIVO_EVENT_REGISTER, VIVO_EVENT_ACTIVATION);
|
|
|
+ case 4 -> List.of(VIVO_EVENT_RETENTION_1);
|
|
|
+ case 6, 2001 -> List.of(VIVO_EVENT_REACTIVATION);
|
|
|
+ case 7 -> List.of(VIVO_EVENT_OTHER);
|
|
|
+ case 8 -> List.of(VIVO_EVENT_RETENTION_7);
|
|
|
+ case 9 -> List.of(VIVO_EVENT_RETENTION_3);
|
|
|
+ default -> List.of();
|
|
|
+ };
|
|
|
+ if (eventTypes.isEmpty()) {
|
|
|
+ return List.of();
|
|
|
+ }
|
|
|
+ List<VivoTagEventRecord> records = new ArrayList<>(eventTypes.size());
|
|
|
+ for (String eventType : eventTypes) {
|
|
|
+ records.add(buildVivoTagEvent(tagId, baiduAct, eventType));
|
|
|
+ }
|
|
|
+ return records;
|
|
|
+ }
|
|
|
+
|
|
|
// ─── Step 3: 统一落百度转化明细 ───────────────────────────────────────────
|
|
|
|
|
|
private void saveConversions(List<ConversionRecord> records) {
|
|
|
@@ -534,6 +683,7 @@ public class ConversionSyncService {
|
|
|
existing.addAll(collectTencentSuccessfulCallbackKeys(callbackTasks));
|
|
|
existing.addAll(collectHonorSuccessfulCallbackKeys(callbackTasks));
|
|
|
existing.addAll(collectKuaishouSuccessfulCallbackKeys(callbackTasks));
|
|
|
+ existing.addAll(collectVivoSuccessfulCallbackKeys(callbackTasks));
|
|
|
|
|
|
List<CallbackTask> pending = new ArrayList<>(callbackTasks.size());
|
|
|
for (CallbackTask task : callbackTasks) {
|
|
|
@@ -623,6 +773,30 @@ public class ConversionSyncService {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ private Set<String> collectVivoSuccessfulCallbackKeys(List<CallbackTask> callbackTasks) {
|
|
|
+ if (vivoColdStore == null) {
|
|
|
+ return Set.of();
|
|
|
+ }
|
|
|
+
|
|
|
+ Set<String> keys = new HashSet<>();
|
|
|
+ for (CallbackTask task : callbackTasks) {
|
|
|
+ if (task.isVivo()) {
|
|
|
+ keys.add(task.callbackKey);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (keys.isEmpty()) {
|
|
|
+ return Set.of();
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ return vivoColdStore.successfulCallbackKeys(keys);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("[ConversionSync] vivo dedup check error: size={}, {}", keys.size(), e.getMessage(), e);
|
|
|
+ return Set.of();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
// ─── Step 5: 并发回传媒体 ───────────────────────────────────────────────
|
|
|
|
|
|
private List<CallbackDispatchResult> dispatchCallbacks(List<CallbackTask> tasks,
|
|
|
@@ -683,6 +857,17 @@ public class ConversionSyncService {
|
|
|
return CallbackDispatchResult.kuaishou(task, toCallbackRecord(task, kuaishouRecord));
|
|
|
}
|
|
|
|
|
|
+ if (task.isVivo()) {
|
|
|
+ int deductionRate = resolveVivoDeductionRate(task.vivoBid);
|
|
|
+ if (handleVivoDeduction(task)) {
|
|
|
+ return CallbackDispatchResult.vivo(task, toCallbackRecord(task, buildDeductedVivoCallbackRecord(task, deductionRate)));
|
|
|
+ }
|
|
|
+ VivoMediaCallbackRecord vivoRecord = vivoClient.sendConversion(
|
|
|
+ task.vivoBid, task.payment, task.vivoTagEvent, true);
|
|
|
+ vivoRecord.setDeductionRate(deductionRate);
|
|
|
+ return CallbackDispatchResult.vivo(task, toCallbackRecord(task, vivoRecord));
|
|
|
+ }
|
|
|
+
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
@@ -696,11 +881,14 @@ public class ConversionSyncService {
|
|
|
List<MediaCallbackRecord> tencentRecords = new ArrayList<>();
|
|
|
List<HonorMediaCallbackRecord> honorRecords = new ArrayList<>();
|
|
|
List<KuaishouMediaCallbackRecord> kuaishouRecords = new ArrayList<>();
|
|
|
+ List<VivoMediaCallbackRecord> vivoRecords = new ArrayList<>();
|
|
|
for (CallbackDispatchResult result : results) {
|
|
|
if (result.isHonor()) {
|
|
|
honorRecords.add(toHonorCallbackRecord(result.task, result.record));
|
|
|
} else if (result.isKuaishou()) {
|
|
|
kuaishouRecords.add(toKuaishouCallbackRecord(result.task, result.record));
|
|
|
+ } else if (result.isVivo()) {
|
|
|
+ vivoRecords.add(toVivoCallbackRecord(result.task, result.record));
|
|
|
} else {
|
|
|
tencentRecords.add(result.record);
|
|
|
}
|
|
|
@@ -729,6 +917,14 @@ public class ConversionSyncService {
|
|
|
log.error("[ConversionSync] batch save kuaishou callbacks failed: size={}, {}", kuaishouRecords.size(), e.getMessage(), e);
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ if (vivoColdStore != null && !vivoRecords.isEmpty()) {
|
|
|
+ try {
|
|
|
+ vivoColdStore.saveMediaCallbacks(vivoRecords);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("[ConversionSync] batch save vivo callbacks failed: size={}, {}", vivoRecords.size(), e.getMessage(), e);
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
// ─── 工具方法 ────────────────────────────────────────────────────────────
|
|
|
@@ -811,6 +1007,26 @@ public class ConversionSyncService {
|
|
|
return record;
|
|
|
}
|
|
|
|
|
|
+ private static ConversionRecord toVivoConversionRecord(PaymentInfo payment, VivoBidRecord bid) {
|
|
|
+ ConversionRecord record = new ConversionRecord();
|
|
|
+ record.setDedupeKey(conversionDedupeKey(payment));
|
|
|
+ record.setQk(payment.getQk());
|
|
|
+ record.setMedia("vivo");
|
|
|
+ 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) {
|
|
|
return String.format("%s:%s:%s:%d",
|
|
|
payment.getQk(),
|
|
|
@@ -860,6 +1076,16 @@ public class ConversionSyncService {
|
|
|
eventType);
|
|
|
}
|
|
|
|
|
|
+ static String vivoCallbackDedupeKey(PaymentInfo payment, VivoBidRecord bid, String eventType) {
|
|
|
+ return String.format("vivo:%s:%s:%s:%s:%d:%s",
|
|
|
+ payment.getQk(),
|
|
|
+ payment.getDate(),
|
|
|
+ payment.getDeviceId(),
|
|
|
+ bid.getTagId(),
|
|
|
+ payment.getAct(),
|
|
|
+ eventType);
|
|
|
+ }
|
|
|
+
|
|
|
private DeductionHandling handleTencentDeduction(CallbackTask task) {
|
|
|
if (!task.isTencent() || task.tencentBid == null) {
|
|
|
return DeductionHandling.SEND;
|
|
|
@@ -986,6 +1212,57 @@ public class ConversionSyncService {
|
|
|
return record;
|
|
|
}
|
|
|
|
|
|
+ private boolean handleVivoDeduction(CallbackTask task) {
|
|
|
+ if (!task.isVivo() || task.vivoBid == null || vivoHotStore == null) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ int deductionRate = resolveVivoDeductionRate(task.vivoBid);
|
|
|
+ if (task.payment.getAct() == 2001 || deductionRate <= 0) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (!vivoHotStore.markConversionSeen(task.callbackKey)) {
|
|
|
+ log.info("[ConversionSync] vivo 转化已见过,跳过重复扣量判断 | callbackKey={} | qk={} | accountId={}",
|
|
|
+ task.callbackKey, task.payment.getQk(), safe(task.vivoBid.getAccountId()));
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ VivoHotStore.DeductionDecision decision = vivoHotStore.evaluateDeduction(
|
|
|
+ task.vivoBid.getAccountId(),
|
|
|
+ task.payment.getAct(),
|
|
|
+ task.payment.getDate(),
|
|
|
+ deductionRate
|
|
|
+ );
|
|
|
+ boolean deduct = decision.isShouldDeduct();
|
|
|
+ log.info("[ConversionSync] vivo 转化扣量判定 | qk={} | act={} | accountId={} | tagId={} | rate={} | totalSeen={} | totalDeducted={} | callbackKey={} | deduct={}",
|
|
|
+ task.payment.getQk(), task.payment.getAct(), safe(task.vivoBid.getAccountId()),
|
|
|
+ safe(task.vivoBid.getTagId()), deductionRate,
|
|
|
+ decision.getTotalSeen(), decision.getTotalDeducted(), task.callbackKey, deduct);
|
|
|
+ return deduct;
|
|
|
+ }
|
|
|
+
|
|
|
+ private VivoMediaCallbackRecord buildDeductedVivoCallbackRecord(CallbackTask task, int deductionRate) {
|
|
|
+ VivoMediaCallbackRecord record = new VivoMediaCallbackRecord();
|
|
|
+ Instant now = Instant.now();
|
|
|
+ record.setDedupeKey(task.callbackKey);
|
|
|
+ record.setQk(task.payment.getQk());
|
|
|
+ record.setTagId(task.vivoBid.getTagId());
|
|
|
+ record.setCallbackUrl("");
|
|
|
+ record.setEventType(task.vivoTagEvent.getEventType());
|
|
|
+ record.setEventTimeMs(now.toEpochMilli());
|
|
|
+ record.setPurchase(Math.max(task.payment.getPayment(), 0D));
|
|
|
+ record.setDeductionRate(deductionRate);
|
|
|
+ record.setDirectMatch(false);
|
|
|
+ record.setDispatchStatus(VivoMediaCallbackRecord.DISPATCH_STATUS_DEDUCTED);
|
|
|
+ record.setStatus(0);
|
|
|
+ record.setOk(false);
|
|
|
+ record.setAttempt(1);
|
|
|
+ record.setErrorMessage("deducted_by_rate");
|
|
|
+ record.setCreatedAt(now);
|
|
|
+ log.info("[ConversionSync] vivo 转化扣量 | qk={} | act={} | rate={} | tagId={} | accountId={} | callbackKey={}",
|
|
|
+ task.payment.getQk(), task.payment.getAct(), deductionRate,
|
|
|
+ task.vivoBid.getTagId(), safe(task.vivoBid.getAccountId()), task.callbackKey);
|
|
|
+ return record;
|
|
|
+ }
|
|
|
+
|
|
|
private static String resolveTrackingVersion(BidRecord bid) {
|
|
|
if (bid == null || bid.getMediaParams() == null) {
|
|
|
return "v1";
|
|
|
@@ -1022,6 +1299,20 @@ public class ConversionSyncService {
|
|
|
return 0;
|
|
|
}
|
|
|
|
|
|
+ private int resolveVivoDeductionRate(VivoBidRecord bid) {
|
|
|
+ if (bid == null || bid.getMediaParams() == null) {
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+ String rawRate = bid.getMediaParams().get(VivoBidRecord.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) {
|
|
|
return value == null ? "" : value;
|
|
|
}
|
|
|
@@ -1074,6 +1365,31 @@ public class ConversionSyncService {
|
|
|
return callback;
|
|
|
}
|
|
|
|
|
|
+ private static VivoMediaCallbackRecord toVivoCallbackRecord(CallbackTask task, MediaCallbackRecord record) {
|
|
|
+ VivoMediaCallbackRecord callback = new VivoMediaCallbackRecord();
|
|
|
+ callback.setDedupeKey(record.getDedupeKey());
|
|
|
+ callback.setQk(record.getQk());
|
|
|
+ callback.setTagId(task.tagId);
|
|
|
+ callback.setCallbackUrl(record.getCallbackUrl());
|
|
|
+ callback.setEventType(record.getEventType() == 0 ? null : String.valueOf(record.getEventType()));
|
|
|
+ if (task.vivoTagEvent != null) {
|
|
|
+ callback.setEventType(task.vivoTagEvent.getEventType());
|
|
|
+ }
|
|
|
+ callback.setEventTimeMs(record.getEventTimeMs());
|
|
|
+ callback.setPurchase(record.getPurchase());
|
|
|
+ callback.setDeductionRate(parseVivoDeductionRate(task.vivoBid));
|
|
|
+ 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(!VivoMediaCallbackRecord.DISPATCH_STATUS_DEDUCTED.equals(record.getDispatchStatus()));
|
|
|
+ callback.setCreatedAt(record.getCreatedAt());
|
|
|
+ return callback;
|
|
|
+ }
|
|
|
+
|
|
|
private static MediaCallbackRecord toCallbackRecord(CallbackTask task, HonorMediaCallbackRecord honorRecord) {
|
|
|
MediaCallbackRecord callbackRecord = new MediaCallbackRecord();
|
|
|
callbackRecord.setDedupeKey(task.callbackKey);
|
|
|
@@ -1115,6 +1431,27 @@ public class ConversionSyncService {
|
|
|
return callbackRecord;
|
|
|
}
|
|
|
|
|
|
+ private static MediaCallbackRecord toCallbackRecord(CallbackTask task, VivoMediaCallbackRecord vivoRecord) {
|
|
|
+ MediaCallbackRecord callbackRecord = new MediaCallbackRecord();
|
|
|
+ callbackRecord.setDedupeKey(task.callbackKey);
|
|
|
+ callbackRecord.setMedia("vivo");
|
|
|
+ callbackRecord.setQk(vivoRecord.getQk());
|
|
|
+ callbackRecord.setCallbackUrl(vivoRecord.getCallbackUrl());
|
|
|
+ callbackRecord.setEventType(0);
|
|
|
+ callbackRecord.setEventTimeMs(vivoRecord.getEventTimeMs());
|
|
|
+ callbackRecord.setPurchase(vivoRecord.getPurchase());
|
|
|
+ callbackRecord.setStatus(vivoRecord.getStatus());
|
|
|
+ callbackRecord.setOk(vivoRecord.isOk());
|
|
|
+ callbackRecord.setAttempt(vivoRecord.getAttempt());
|
|
|
+ callbackRecord.setResponseBody(vivoRecord.getResponseBody());
|
|
|
+ callbackRecord.setErrorMessage(vivoRecord.getErrorMessage());
|
|
|
+ callbackRecord.setRequestBody(vivoRecord.getRequestBody());
|
|
|
+ callbackRecord.setDispatchStatus(vivoRecord.getDispatchStatus());
|
|
|
+ callbackRecord.setTrackingVersion(vivoRecord.isDirectMatch() ? "direct" : "deducted");
|
|
|
+ callbackRecord.setCreatedAt(vivoRecord.getCreatedAt());
|
|
|
+ return callbackRecord;
|
|
|
+ }
|
|
|
+
|
|
|
private static int parseKuaishouDeductionRate(KuaishouBidRecord bid) {
|
|
|
if (bid == null || bid.getMediaParams() == null) {
|
|
|
return 0;
|
|
|
@@ -1134,19 +1471,41 @@ public class ConversionSyncService {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ private static int parseVivoDeductionRate(VivoBidRecord bid) {
|
|
|
+ if (bid == null || bid.getMediaParams() == null) {
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+ String rawRate = bid.getMediaParams().get(VivoBidRecord.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 final Map<String, BidRecord> tencentBids;
|
|
|
private final Map<String, HonorBidRecord> honorBids;
|
|
|
private final Map<String, KuaishouBidRecord> kuaishouBids;
|
|
|
+ private final Map<String, VivoBidRecord> vivoBids;
|
|
|
|
|
|
private BidLookup(Map<String, BidRecord> tencentBids,
|
|
|
Map<String, HonorBidRecord> honorBids,
|
|
|
- Map<String, KuaishouBidRecord> kuaishouBids) {
|
|
|
+ Map<String, KuaishouBidRecord> kuaishouBids,
|
|
|
+ Map<String, VivoBidRecord> vivoBids) {
|
|
|
this.tencentBids = tencentBids;
|
|
|
this.honorBids = honorBids;
|
|
|
this.kuaishouBids = kuaishouBids;
|
|
|
+ this.vivoBids = vivoBids;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -1161,6 +1520,8 @@ public class ConversionSyncService {
|
|
|
private final Integer honorConversionId;
|
|
|
private final KuaishouBidRecord kuaishouBid;
|
|
|
private final KuaishouTagEventRecord kuaishouTagEvent;
|
|
|
+ private final VivoBidRecord vivoBid;
|
|
|
+ private final VivoTagEventRecord vivoTagEvent;
|
|
|
|
|
|
private CallbackTask(String media,
|
|
|
PaymentInfo payment,
|
|
|
@@ -1171,7 +1532,9 @@ public class ConversionSyncService {
|
|
|
HonorBidRecord honorBid,
|
|
|
Integer honorConversionId,
|
|
|
KuaishouBidRecord kuaishouBid,
|
|
|
- KuaishouTagEventRecord kuaishouTagEvent) {
|
|
|
+ KuaishouTagEventRecord kuaishouTagEvent,
|
|
|
+ VivoBidRecord vivoBid,
|
|
|
+ VivoTagEventRecord vivoTagEvent) {
|
|
|
this.media = media;
|
|
|
this.payment = payment;
|
|
|
this.tagId = tagId;
|
|
|
@@ -1182,27 +1545,36 @@ public class ConversionSyncService {
|
|
|
this.honorConversionId = honorConversionId;
|
|
|
this.kuaishouBid = kuaishouBid;
|
|
|
this.kuaishouTagEvent = kuaishouTagEvent;
|
|
|
+ this.vivoBid = vivoBid;
|
|
|
+ this.vivoTagEvent = vivoTagEvent;
|
|
|
}
|
|
|
|
|
|
private static CallbackTask tencent(PaymentInfo payment,
|
|
|
BidRecord bid,
|
|
|
String callbackKey,
|
|
|
String actionType) {
|
|
|
- return new CallbackTask("tencent", payment, bid.getTagId(), callbackKey, bid, actionType, null, null, null, null);
|
|
|
+ return new CallbackTask("tencent", payment, bid.getTagId(), callbackKey, bid, actionType, null, null, null, null, null, null);
|
|
|
}
|
|
|
|
|
|
private static CallbackTask honor(PaymentInfo payment,
|
|
|
HonorBidRecord bid,
|
|
|
String callbackKey,
|
|
|
Integer conversionId) {
|
|
|
- return new CallbackTask("honor", payment, bid.getTagId(), callbackKey, null, null, bid, conversionId, null, null);
|
|
|
+ return new CallbackTask("honor", payment, bid.getTagId(), callbackKey, null, null, bid, conversionId, null, null, 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);
|
|
|
+ return new CallbackTask("kuaishou", payment, bid.getTagId(), callbackKey, null, null, null, null, bid, tagEvent, null, null);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static CallbackTask vivo(PaymentInfo payment,
|
|
|
+ VivoBidRecord bid,
|
|
|
+ String callbackKey,
|
|
|
+ VivoTagEventRecord tagEvent) {
|
|
|
+ return new CallbackTask("vivo", payment, bid.getTagId(), callbackKey, null, null, null, null, null, null, bid, tagEvent);
|
|
|
}
|
|
|
|
|
|
private boolean isTencent() {
|
|
|
@@ -1216,6 +1588,10 @@ public class ConversionSyncService {
|
|
|
private boolean isKuaishou() {
|
|
|
return "kuaishou".equals(media);
|
|
|
}
|
|
|
+
|
|
|
+ private boolean isVivo() {
|
|
|
+ return "vivo".equals(media);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
private static class CallbackDispatchResult {
|
|
|
@@ -1241,6 +1617,10 @@ public class ConversionSyncService {
|
|
|
return new CallbackDispatchResult("kuaishou", task, record);
|
|
|
}
|
|
|
|
|
|
+ private static CallbackDispatchResult vivo(CallbackTask task, MediaCallbackRecord record) {
|
|
|
+ return new CallbackDispatchResult("vivo", task, record);
|
|
|
+ }
|
|
|
+
|
|
|
private boolean isHonor() {
|
|
|
return "honor".equals(media);
|
|
|
}
|
|
|
@@ -1248,6 +1628,10 @@ public class ConversionSyncService {
|
|
|
private boolean isKuaishou() {
|
|
|
return "kuaishou".equals(media);
|
|
|
}
|
|
|
+
|
|
|
+ private boolean isVivo() {
|
|
|
+ return "vivo".equals(media);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
private enum DeductionHandling {
|