ConversionSyncService.java 77 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727
  1. package com.adx.tencent.conversionsync;
  2. import com.adx.tencent.baidu.ConversionClient;
  3. import com.adx.tencent.baidu.model.ConversionQuery;
  4. import com.adx.tencent.baidu.model.ConversionResponse;
  5. import com.adx.tencent.baidu.model.PaymentInfo;
  6. import com.adx.tencent.config.AppProperties;
  7. import com.adx.tencent.honor.client.HonorClient;
  8. import com.adx.tencent.honor.model.HonorBidRecord;
  9. import com.adx.tencent.honor.model.HonorMediaCallbackRecord;
  10. import com.adx.tencent.honor.service.HonorTagEventResolver;
  11. import com.adx.tencent.honor.store.HonorColdStore;
  12. import com.adx.tencent.honor.store.HonorHotStore;
  13. import com.adx.tencent.kuaishou.client.KuaishouClient;
  14. import com.adx.tencent.kuaishou.model.KuaishouBidRecord;
  15. import com.adx.tencent.kuaishou.model.KuaishouMediaCallbackRecord;
  16. import com.adx.tencent.kuaishou.model.KuaishouTagEventRecord;
  17. import com.adx.tencent.kuaishou.service.KuaishouTagEventResolver;
  18. import com.adx.tencent.kuaishou.store.KuaishouColdStore;
  19. import com.adx.tencent.kuaishou.store.KuaishouHotStore;
  20. import com.adx.tencent.storage.RedisHotStore;
  21. import com.adx.tencent.storage.TiDBColdStore;
  22. import com.adx.tencent.storage.model.BidRecord;
  23. import com.adx.tencent.storage.model.ConversionRecord;
  24. import com.adx.tencent.storage.model.MediaCallbackRecord;
  25. import com.adx.tencent.storage.model.AccountTagEventRecord;
  26. import com.adx.tencent.tagsync.AccountTagEventResolver;
  27. import com.adx.tencent.tagsync.TagEventResolver;
  28. import com.adx.tencent.tencent.TencentClient;
  29. import com.adx.tencent.vivo.client.VivoClient;
  30. import com.adx.tencent.vivo.model.VivoBidRecord;
  31. import com.adx.tencent.vivo.model.VivoMediaCallbackRecord;
  32. import com.adx.tencent.vivo.model.VivoTagEventRecord;
  33. import com.adx.tencent.vivo.service.VivoTagEventResolver;
  34. import com.adx.tencent.vivo.store.VivoColdStore;
  35. import com.adx.tencent.vivo.store.VivoHotStore;
  36. import org.slf4j.Logger;
  37. import org.slf4j.LoggerFactory;
  38. import java.time.Instant;
  39. import java.util.ArrayList;
  40. import java.util.HashSet;
  41. import java.util.LinkedHashSet;
  42. import java.util.List;
  43. import java.util.Map;
  44. import java.util.Set;
  45. import java.util.concurrent.CompletableFuture;
  46. import java.util.concurrent.ExecutorService;
  47. import java.util.concurrent.Executors;
  48. import java.util.concurrent.TimeUnit;
  49. import java.util.concurrent.atomic.AtomicInteger;
  50. /**
  51. * 统一转化同步:
  52. * 1. 只从百度拉一次转化
  53. * 2. 按 qk 匹配腾讯/荣耀/快手 bid
  54. * 3. 百度转化明细统一落 tencent_baidu_conversions
  55. * 4. 媒体回传结果分别落各自表
  56. */
  57. public class ConversionSyncService {
  58. private static final Logger log = LoggerFactory.getLogger(ConversionSyncService.class);
  59. private static final int REALTIME_CONCURRENCY = 10;
  60. private static final int BACKFILL_CONCURRENCY = 16;
  61. private static final int KS_EVENT_ACTIVATION = 1;
  62. private static final int KS_EVENT_REGISTRATION = 2;
  63. private static final int KS_EVENT_PURCHASE = 3;
  64. private static final int KS_EVENT_NEXT_DAY_RETENTION = 7;
  65. private static final int KS_EVENT_SEVEN_DAY_RETENTION = 8;
  66. private static final int KS_EVENT_APP_WAKEUP = 84;
  67. private static final int KS_EVENT_KEY_ACTION = 143;
  68. private static final int KS_EVENT_THREE_DAY_RETENTION = 103;
  69. private static final String VIVO_EVENT_ACTIVATION = "ACTIVATION";
  70. private static final String VIVO_EVENT_REGISTER = "REGISTER";
  71. private static final String VIVO_EVENT_PAY = "PAY";
  72. private static final String VIVO_EVENT_RETENTION_1 = "RETENTION_1";
  73. private static final String VIVO_EVENT_RETENTION_3 = "RETENTION_3";
  74. private static final String VIVO_EVENT_RETENTION_7 = "RETENTION_7";
  75. private static final String VIVO_EVENT_REACTIVATION = "REACTIVATION";
  76. private static final String VIVO_EVENT_OTHER = "OTHER";
  77. private final ConversionClient baiduClient;
  78. private final RedisHotStore hotStore;
  79. private final HonorHotStore honorHotStore;
  80. private final KuaishouHotStore kuaishouHotStore;
  81. private final VivoHotStore vivoHotStore;
  82. private final TencentClient tencentClient;
  83. private final TiDBColdStore coldStore;
  84. private final HonorColdStore honorColdStore;
  85. private final KuaishouColdStore kuaishouColdStore;
  86. private final VivoColdStore vivoColdStore;
  87. private final HonorClient honorClient;
  88. private final KuaishouClient kuaishouClient;
  89. private final VivoClient vivoClient;
  90. private final TagEventResolver tagEventResolver;
  91. private final AccountTagEventResolver accountTagEventResolver;
  92. private final HonorTagEventResolver honorTagEventResolver;
  93. private final KuaishouTagEventResolver kuaishouTagEventResolver;
  94. private final VivoTagEventResolver vivoTagEventResolver;
  95. private final ExecutorService realtimeExecutor;
  96. private final ExecutorService backfillExecutor;
  97. public enum SyncMode {
  98. REALTIME,
  99. BACKFILL
  100. }
  101. public ConversionSyncService(ConversionClient baiduClient,
  102. RedisHotStore hotStore,
  103. TencentClient tencentClient,
  104. TiDBColdStore coldStore,
  105. TagEventResolver tagEventResolver,
  106. AccountTagEventResolver accountTagEventResolver,
  107. HonorTagEventResolver honorTagEventResolver,
  108. KuaishouTagEventResolver kuaishouTagEventResolver,
  109. VivoTagEventResolver vivoTagEventResolver,
  110. HonorHotStore honorHotStore,
  111. HonorColdStore honorColdStore,
  112. KuaishouHotStore kuaishouHotStore,
  113. KuaishouColdStore kuaishouColdStore,
  114. VivoHotStore vivoHotStore,
  115. VivoColdStore vivoColdStore,
  116. HonorClient honorClient,
  117. KuaishouClient kuaishouClient,
  118. VivoClient vivoClient,
  119. AppProperties props) {
  120. this.baiduClient = baiduClient;
  121. this.hotStore = hotStore;
  122. this.tencentClient = tencentClient;
  123. this.coldStore = coldStore;
  124. this.tagEventResolver = tagEventResolver;
  125. this.accountTagEventResolver = accountTagEventResolver;
  126. this.honorTagEventResolver = honorTagEventResolver;
  127. this.kuaishouTagEventResolver = kuaishouTagEventResolver;
  128. this.vivoTagEventResolver = vivoTagEventResolver;
  129. this.honorHotStore = honorHotStore;
  130. this.honorColdStore = honorColdStore;
  131. this.kuaishouHotStore = kuaishouHotStore;
  132. this.kuaishouColdStore = kuaishouColdStore;
  133. this.vivoHotStore = vivoHotStore;
  134. this.vivoColdStore = vivoColdStore;
  135. this.honorClient = honorClient;
  136. this.kuaishouClient = kuaishouClient;
  137. this.vivoClient = vivoClient;
  138. this.realtimeExecutor = Executors.newFixedThreadPool(
  139. REALTIME_CONCURRENCY,
  140. r -> {
  141. Thread t = new Thread(r, "conv-sync-worker");
  142. t.setDaemon(true);
  143. return t;
  144. });
  145. this.backfillExecutor = Executors.newFixedThreadPool(
  146. BACKFILL_CONCURRENCY,
  147. r -> {
  148. Thread t = new Thread(r, "conv-backfill-worker");
  149. t.setDaemon(true);
  150. return t;
  151. });
  152. }
  153. public SyncResult syncTencentConversions(ConversionQuery query) throws Exception {
  154. return syncTencentConversions(query, SyncMode.REALTIME);
  155. }
  156. public SyncResult syncTencentConversions(ConversionQuery query, SyncMode mode) throws Exception {
  157. if (query.getPageSize() <= 0) {
  158. query.setPageSize(1);
  159. }
  160. SyncResult result = new SyncResult();
  161. int maxPages = 10000;
  162. for (int page = query.getPageSize(); ; page++) {
  163. query.setPageSize(page);
  164. long pageStartNs = System.nanoTime();
  165. log.info("[ConversionSync] fetching date={}, page={}, mode={}", query.getDate(), page, mode);
  166. ConversionResponse response = baiduClient.queryPayments(query);
  167. List<PaymentInfo> data = response.getData();
  168. int dataSize = data != null ? data.size() : 0;
  169. log.info("[ConversionSync] date={}, page={} returned {} records, responsePageSize={}, mode={}",
  170. query.getDate(), page, dataSize, response.getPageSize(), mode);
  171. if (data != null) {
  172. processPayments(data, result, mode);
  173. }
  174. long pageCostMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - pageStartNs);
  175. log.info("[ConversionSync] date={}, page={} processed, cost={}ms, totalFetched={}, matched={}, sent={}, failed={}, skipped={}, alreadySent={}, mode={}",
  176. query.getDate(), page, pageCostMs,
  177. result.fetched.get(), result.matched.get(), result.sent.get(), result.failed.get(),
  178. result.skipped.get(), result.alreadySent.get(), mode);
  179. if (response.getPageSize() <= page) {
  180. break;
  181. }
  182. if (page >= maxPages) {
  183. log.warn("[ConversionSync] reached max page limit ({}), stopping", maxPages);
  184. break;
  185. }
  186. }
  187. return result;
  188. }
  189. private void processPayments(List<PaymentInfo> payments, SyncResult result, SyncMode mode) {
  190. if (payments == null || payments.isEmpty()) {
  191. return;
  192. }
  193. ExecutorService executor = mode == SyncMode.BACKFILL ? backfillExecutor : realtimeExecutor;
  194. // 1. 批量查这一页转化对应的腾讯/荣耀/快手 bid;Redis miss 时会自动回落到冷库。
  195. BidLookup lookup = loadBidLookup(payments, result);
  196. // 2. 统一整理要落库的百度转化明细,以及要回传给媒体的任务。
  197. List<ConversionRecord> conversionRecords = new ArrayList<>();
  198. List<CallbackTask> callbackTasks = new ArrayList<>();
  199. buildRecordsAndTasks(payments, lookup, conversionRecords, callbackTasks, result);
  200. // 3. 百度转化明细统一落 tencent_baidu_conversions。
  201. saveConversions(conversionRecords);
  202. if (callbackTasks.isEmpty()) {
  203. return;
  204. }
  205. // 4. 回传前先做去重,只发送未成功回传过的任务。
  206. List<CallbackTask> pendingTasks = filterPendingCallbacks(callbackTasks, result);
  207. if (pendingTasks.isEmpty()) {
  208. return;
  209. }
  210. // 5. 并发回传媒体,并把回传结果分别落到腾讯/荣耀回调表。
  211. List<CallbackDispatchResult> dispatchResults = dispatchCallbacks(pendingTasks, executor, result);
  212. saveCallbackResults(dispatchResults);
  213. }
  214. // ─── Step 1: 加载 bid ───────────────────────────────────────────────────
  215. private BidLookup loadBidLookup(List<PaymentInfo> payments, SyncResult result) {
  216. Set<String> qks = new LinkedHashSet<>();
  217. for (PaymentInfo payment : payments) {
  218. result.fetched.incrementAndGet();
  219. if (payment.getQk() != null && !payment.getQk().isBlank()) {
  220. qks.add(payment.getQk());
  221. }
  222. }
  223. List<String> qkList = new ArrayList<>(qks);
  224. Map<String, BidRecord> tencentBids = hotStore.findBidsByQks(qkList);
  225. backfillTencentAccountIds(qkList, tencentBids);
  226. Map<String, HonorBidRecord> honorBids = honorHotStore != null
  227. ? honorHotStore.findBidsByQks(qkList)
  228. : Map.of();
  229. Map<String, KuaishouBidRecord> kuaishouBids = kuaishouHotStore != null
  230. ? kuaishouHotStore.findBidsByQks(qkList)
  231. : Map.of();
  232. Map<String, VivoBidRecord> vivoBids = vivoHotStore != null
  233. ? vivoHotStore.findBidsByQks(qkList)
  234. : Map.of();
  235. return new BidLookup(tencentBids, honorBids, kuaishouBids, vivoBids);
  236. }
  237. private void backfillTencentAccountIds(List<String> qkList, Map<String, BidRecord> tencentBids) {
  238. if (coldStore == null || qkList == null || qkList.isEmpty() || tencentBids == null || tencentBids.isEmpty()) {
  239. return;
  240. }
  241. List<String> missingAccountQks = new ArrayList<>();
  242. for (String qk : qkList) {
  243. BidRecord bid = tencentBids.get(qk);
  244. if (bid != null
  245. && "tencent".equals(bid.getMedia())
  246. && (bid.getAccountId() == null || bid.getAccountId().isBlank())) {
  247. missingAccountQks.add(qk);
  248. }
  249. }
  250. if (missingAccountQks.isEmpty()) {
  251. return;
  252. }
  253. try {
  254. for (BidRecord coldBid : coldStore.getBidsByQks(missingAccountQks)) {
  255. if (coldBid == null || coldBid.getQk() == null) {
  256. continue;
  257. }
  258. BidRecord hotBid = tencentBids.get(coldBid.getQk());
  259. if (hotBid == null || hotBid.getAccountId() != null && !hotBid.getAccountId().isBlank()) {
  260. continue;
  261. }
  262. if (coldBid.getAccountId() == null || coldBid.getAccountId().isBlank()) {
  263. continue;
  264. }
  265. hotBid.setAccountId(coldBid.getAccountId());
  266. log.info("[ConversionSync] 腾讯 bid accountId 回落冷库补齐 | qk={} | accountId={}",
  267. coldBid.getQk(), coldBid.getAccountId());
  268. }
  269. } catch (Exception e) {
  270. log.warn("[ConversionSync] 腾讯 bid accountId 回落冷库失败 | size={} | error={}",
  271. missingAccountQks.size(), e.getMessage(), e);
  272. }
  273. }
  274. // ─── Step 2: 组装转化记录与回传任务 ───────────────────────────────────────
  275. private void buildRecordsAndTasks(List<PaymentInfo> payments,
  276. BidLookup lookup,
  277. List<ConversionRecord> conversionRecords,
  278. List<CallbackTask> callbackTasks,
  279. SyncResult result) {
  280. for (PaymentInfo payment : payments) {
  281. BidRecord tencentBid = lookup.tencentBids.get(payment.getQk());
  282. HonorBidRecord honorBid = lookup.honorBids.get(payment.getQk());
  283. KuaishouBidRecord kuaishouBid = lookup.kuaishouBids.get(payment.getQk());
  284. VivoBidRecord vivoBid = lookup.vivoBids.get(payment.getQk());
  285. if (tencentBid == null && honorBid == null && kuaishouBid == null && vivoBid == null) {
  286. conversionRecords.add(toConversionRecord(payment, null, ""));
  287. result.skipped.incrementAndGet();
  288. continue;
  289. }
  290. boolean hasTencentBid = tencentBid != null;
  291. boolean hasHonorBid = honorBid != null;
  292. boolean hasKuaishouBid = kuaishouBid != null;
  293. boolean hasVivoBid = vivoBid != null;
  294. boolean validTencent = hasTencentBid && "tencent".equals(tencentBid.getMedia());
  295. boolean validHonor = hasHonorBid && "honor".equals(honorBid.getMedia());
  296. boolean validKuaishou = hasKuaishouBid && "kuaishou".equals(kuaishouBid.getMedia());
  297. boolean validVivo = hasVivoBid && "vivo".equals(vivoBid.getMedia());
  298. if (hasTencentBid && !validTencent) {
  299. log.warn("[ConversionSync] 腾讯 bid media 非法 | qk={} | media={}", payment.getQk(), tencentBid.getMedia());
  300. }
  301. if (hasHonorBid && !validHonor) {
  302. log.warn("[ConversionSync] 荣耀 bid media 非法 | qk={} | media={}", payment.getQk(), honorBid.getMedia());
  303. }
  304. if (hasKuaishouBid && !validKuaishou) {
  305. log.warn("[ConversionSync] 快手 bid media 非法 | qk={} | media={}", payment.getQk(), kuaishouBid.getMedia());
  306. }
  307. if (hasVivoBid && !validVivo) {
  308. log.warn("[ConversionSync] vivo bid media 非法 | qk={} | media={}", payment.getQk(), vivoBid.getMedia());
  309. }
  310. int matchedMediaCount = (validTencent ? 1 : 0) + (validHonor ? 1 : 0) + (validKuaishou ? 1 : 0) + (validVivo ? 1 : 0);
  311. if (matchedMediaCount > 1) {
  312. log.warn("[ConversionSync] qk 同时命中多媒体,跳过分发 | qk={} | act={} | date={}",
  313. payment.getQk(), payment.getAct(), payment.getDate());
  314. result.skipped.incrementAndGet();
  315. continue;
  316. }
  317. if (validTencent) {
  318. conversionRecords.add(toConversionRecord(payment, tencentBid, "tencent"));
  319. result.matched.incrementAndGet();
  320. log.info("[ConversionSync] 匹配到腾讯转化 | qk={} | act={} | deviceId={} | date={} | payment={}",
  321. payment.getQk(), payment.getAct(), payment.getDeviceId(), payment.getDate(), payment.getPayment());
  322. CallbackTask tencentTask = buildTencentCallbackTask(payment, tencentBid);
  323. if (tencentTask != null) {
  324. callbackTasks.add(tencentTask);
  325. }
  326. continue;
  327. }
  328. if (validHonor) {
  329. conversionRecords.add(toHonorConversionRecord(payment, honorBid));
  330. result.matched.incrementAndGet();
  331. log.info("[ConversionSync] 匹配到荣耀转化 | qk={} | act={} | deviceId={} | date={} | payment={}",
  332. payment.getQk(), payment.getAct(), payment.getDeviceId(), payment.getDate(), payment.getPayment());
  333. CallbackTask honorTask = buildHonorCallbackTask(payment, honorBid);
  334. if (honorTask != null) {
  335. callbackTasks.add(honorTask);
  336. }
  337. continue;
  338. }
  339. if (validKuaishou) {
  340. conversionRecords.add(toKuaishouConversionRecord(payment, kuaishouBid));
  341. result.matched.incrementAndGet();
  342. log.info("[ConversionSync] 匹配到快手转化 | qk={} | act={} | deviceId={} | date={} | payment={}",
  343. payment.getQk(), payment.getAct(), payment.getDeviceId(), payment.getDate(), payment.getPayment());
  344. List<CallbackTask> kuaishouTasks = buildKuaishouCallbackTasks(payment, kuaishouBid);
  345. if (!kuaishouTasks.isEmpty()) {
  346. callbackTasks.addAll(kuaishouTasks);
  347. }
  348. continue;
  349. }
  350. if (validVivo) {
  351. conversionRecords.add(toVivoConversionRecord(payment, vivoBid));
  352. result.matched.incrementAndGet();
  353. log.info("[ConversionSync] 匹配到 vivo 转化 | qk={} | act={} | deviceId={} | date={} | payment={}",
  354. payment.getQk(), payment.getAct(), payment.getDeviceId(), payment.getDate(), payment.getPayment());
  355. List<CallbackTask> vivoTasks = buildVivoCallbackTasks(payment, vivoBid);
  356. if (!vivoTasks.isEmpty()) {
  357. callbackTasks.addAll(vivoTasks);
  358. }
  359. continue;
  360. }
  361. conversionRecords.add(toConversionRecord(payment, hasTencentBid ? tencentBid : null, hasTencentBid ? tencentBid.getMedia() : ""));
  362. result.skipped.incrementAndGet();
  363. }
  364. }
  365. private CallbackTask buildTencentCallbackTask(PaymentInfo payment, BidRecord bid) {
  366. String actionType = null;
  367. String customAction = null;
  368. if (accountTagEventResolver != null) {
  369. AccountTagEventRecord accountMapping = accountTagEventResolver.resolveAction(
  370. bid.getTagId(), bid.getAccountId(), payment.getAct());
  371. if (accountMapping != null) {
  372. actionType = accountMapping.getTencentActionType();
  373. customAction = accountMapping.getTencentCustomAction();
  374. if ("CUSTOM".equals(actionType) && (customAction == null || customAction.isBlank())) {
  375. log.warn("[ConversionSync] 账户级 CUSTOM 回传缺少 custom_action, 忽略账户级配置并回退广告位配置 | qk={} | tagId={} | accountId={} | act={}",
  376. payment.getQk(), bid.getTagId(), bid.getAccountId(), payment.getAct());
  377. actionType = null;
  378. customAction = null;
  379. }
  380. }
  381. }
  382. if (tagEventResolver != null) {
  383. if (actionType == null || actionType.isBlank()) {
  384. actionType = tagEventResolver.resolveActionType(bid.getTagId(), payment.getAct());
  385. customAction = null;
  386. }
  387. }
  388. if (actionType == null || actionType.isBlank()) {
  389. try {
  390. actionType = tencentClient.conversionActionType(payment.getAct());
  391. log.warn("[ConversionSync] 账户级/广告位回传方式未配置, 回退act-map | qk={} | tagId={} | accountId={} | act={} | actionType={}",
  392. payment.getQk(), bid.getTagId(), bid.getAccountId(), payment.getAct(), actionType);
  393. } catch (IllegalArgumentException e) {
  394. // act 既没配广告位回传方式,也没配 act-map:只落库不回传,避免整个任务中断
  395. log.warn("[ConversionSync] 腾讯回传方式未配置, 跳过回传 | qk={} | tagId={} | accountId={} | act={} | date={} | error={}",
  396. payment.getQk(), bid.getTagId(), bid.getAccountId(), payment.getAct(), payment.getDate(), e.getMessage());
  397. return null;
  398. }
  399. }
  400. return CallbackTask.tencent(
  401. payment,
  402. bid,
  403. callbackDedupeKey(payment, bid, actionType),
  404. actionType,
  405. customAction
  406. );
  407. }
  408. private CallbackTask buildHonorCallbackTask(PaymentInfo payment, HonorBidRecord bid) {
  409. if (honorColdStore == null && honorTagEventResolver == null) {
  410. return null;
  411. }
  412. Integer conversionId = honorTagEventResolver != null
  413. ? honorTagEventResolver.resolveConversionId(bid.getTagId(), payment.getAct())
  414. : resolveHonorConversionIdFromDb(bid.getTagId(), payment.getAct());
  415. if (conversionId == null) {
  416. conversionId = fallbackHonorConversionId(payment.getAct());
  417. if (conversionId == null) {
  418. log.warn("[ConversionSync] 荣耀 conversionId 未配置且无兜底映射 | qk={} | tagId={} | act={}",
  419. payment.getQk(), bid.getTagId(), payment.getAct());
  420. return null;
  421. }
  422. log.warn("[ConversionSync] 荣耀 conversionId 未配置, 启用兜底映射 | qk={} | tagId={} | act={} | conversionId={}",
  423. payment.getQk(), bid.getTagId(), payment.getAct(), conversionId);
  424. }
  425. return CallbackTask.honor(
  426. payment,
  427. bid,
  428. honorCallbackDedupeKey(payment, bid, conversionId),
  429. conversionId
  430. );
  431. }
  432. private static Integer fallbackHonorConversionId(int baiduAct) {
  433. return switch (baiduAct) {
  434. case 1 -> 10001; // 激活
  435. case 2, 5 -> 10004; // 付费
  436. case 3 -> 10002; // 注册
  437. case 4 -> 10003; // 次留
  438. case 6, 2001 -> 10022;// 拉活
  439. case 8 -> 10014; // 7日留存
  440. case 9 -> 10013; // 3日留存
  441. default -> null;
  442. };
  443. }
  444. private List<CallbackTask> buildKuaishouCallbackTasks(PaymentInfo payment, KuaishouBidRecord bid) {
  445. if (kuaishouColdStore == null && kuaishouTagEventResolver == null) {
  446. return List.of();
  447. }
  448. List<KuaishouTagEventRecord> tagEvents = resolveKuaishouTagEvents(bid.getTagId(), payment.getAct());
  449. if (tagEvents.isEmpty()) {
  450. log.warn("[ConversionSync] 快手 eventType 未配置且无兜底映射 | qk={} | tagId={} | act={}",
  451. payment.getQk(), bid.getTagId(), payment.getAct());
  452. return List.of();
  453. }
  454. List<CallbackTask> tasks = new ArrayList<>(tagEvents.size());
  455. for (KuaishouTagEventRecord tagEvent : tagEvents) {
  456. tasks.add(CallbackTask.kuaishou(
  457. payment,
  458. bid,
  459. kuaishouCallbackDedupeKey(payment, bid, tagEvent.getEventType()),
  460. tagEvent
  461. ));
  462. }
  463. return tasks;
  464. }
  465. private List<CallbackTask> buildVivoCallbackTasks(PaymentInfo payment, VivoBidRecord bid) {
  466. if (vivoColdStore == null && vivoTagEventResolver == null) {
  467. return List.of();
  468. }
  469. List<VivoTagEventRecord> tagEvents = resolveVivoTagEvents(bid.getTagId(), payment.getAct());
  470. if (tagEvents.isEmpty()) {
  471. log.warn("[ConversionSync] vivo eventType 未配置且无兜底映射 | qk={} | tagId={} | act={}",
  472. payment.getQk(), bid.getTagId(), payment.getAct());
  473. return List.of();
  474. }
  475. List<CallbackTask> tasks = new ArrayList<>(tagEvents.size());
  476. for (VivoTagEventRecord tagEvent : tagEvents) {
  477. tasks.add(CallbackTask.vivo(
  478. payment,
  479. bid,
  480. vivoCallbackDedupeKey(payment, bid, tagEvent.getEventType()),
  481. tagEvent
  482. ));
  483. }
  484. return tasks;
  485. }
  486. private Integer resolveHonorConversionIdFromDb(String tagId, int baiduAct) {
  487. if (honorColdStore == null) {
  488. return null;
  489. }
  490. com.adx.tencent.honor.model.HonorTagEventRecord exact = honorColdStore.getTagEvent(tagId, baiduAct);
  491. if (exact != null) {
  492. return exact.getConversionId();
  493. }
  494. com.adx.tencent.honor.model.HonorTagEventRecord fallback = honorColdStore.getTagEvent(tagId, 0);
  495. return fallback != null ? fallback.getConversionId() : null;
  496. }
  497. private List<KuaishouTagEventRecord> resolveKuaishouTagEvents(String tagId, int baiduAct) {
  498. List<KuaishouTagEventRecord> configured = resolveConfiguredKuaishouTagEvents(tagId, baiduAct);
  499. if (!configured.isEmpty()) {
  500. return configured;
  501. }
  502. List<KuaishouTagEventRecord> fallback = buildKuaishouFallbackTagEvents(tagId, baiduAct);
  503. if (!fallback.isEmpty()) {
  504. log.warn("[ConversionSync] 快手 eventType 未配置, 启用兜底映射 | tagId={} | act={} | fallbackEventTypes={}",
  505. tagId, baiduAct, fallback.stream().map(KuaishouTagEventRecord::getEventType).toList());
  506. }
  507. return fallback;
  508. }
  509. private List<VivoTagEventRecord> resolveVivoTagEvents(String tagId, int baiduAct) {
  510. List<VivoTagEventRecord> configured = resolveConfiguredVivoTagEvents(tagId, baiduAct);
  511. if (!configured.isEmpty()) {
  512. return configured;
  513. }
  514. List<VivoTagEventRecord> fallback = buildVivoFallbackTagEvents(tagId, baiduAct);
  515. if (!fallback.isEmpty()) {
  516. log.warn("[ConversionSync] vivo eventType 未配置, 启用兜底映射 | tagId={} | act={} | fallbackEventTypes={}",
  517. tagId, baiduAct, fallback.stream().map(VivoTagEventRecord::getEventType).toList());
  518. }
  519. return fallback;
  520. }
  521. private List<KuaishouTagEventRecord> resolveConfiguredKuaishouTagEvents(String tagId, int baiduAct) {
  522. if (tagId == null || tagId.isBlank()) {
  523. return List.of();
  524. }
  525. try {
  526. if (kuaishouTagEventResolver != null) {
  527. List<Integer> eventTypes = kuaishouTagEventResolver.resolveEventTypes(tagId, baiduAct);
  528. if (eventTypes != null && !eventTypes.isEmpty()) {
  529. List<KuaishouTagEventRecord> records = new ArrayList<>(eventTypes.size());
  530. for (Integer eventType : eventTypes) {
  531. records.add(buildKuaishouTagEvent(tagId, baiduAct, eventType));
  532. }
  533. return records;
  534. }
  535. return List.of();
  536. }
  537. if (kuaishouColdStore == null) {
  538. return List.of();
  539. }
  540. List<KuaishouTagEventRecord> records = kuaishouColdStore.getTagEvents(tagId, baiduAct);
  541. if (records == null || records.isEmpty()) {
  542. records = kuaishouColdStore.getTagEvents(tagId, 0);
  543. }
  544. return records == null ? List.of() : records;
  545. } catch (Exception e) {
  546. log.warn("[ConversionSync] 查询快手 tag event 失败 | tagId={} | act={} | error={}",
  547. tagId, baiduAct, e.getMessage(), e);
  548. return List.of();
  549. }
  550. }
  551. private static KuaishouTagEventRecord buildKuaishouTagEvent(String tagId, int baiduAct, int eventType) {
  552. KuaishouTagEventRecord record = new KuaishouTagEventRecord();
  553. record.setTagId(tagId);
  554. record.setBaiduAct(baiduAct);
  555. record.setEventType(eventType);
  556. return record;
  557. }
  558. private static List<KuaishouTagEventRecord> buildKuaishouFallbackTagEvents(String tagId, int baiduAct) {
  559. List<Integer> eventTypes = switch (baiduAct) {
  560. case 1 -> List.of(KS_EVENT_ACTIVATION);
  561. case 2, 5 -> List.of(KS_EVENT_PURCHASE);
  562. case 3 -> List.of(KS_EVENT_REGISTRATION, KS_EVENT_ACTIVATION);
  563. case 4 -> List.of(KS_EVENT_NEXT_DAY_RETENTION);
  564. case 6 -> List.of(KS_EVENT_APP_WAKEUP);
  565. case 7 -> List.of(KS_EVENT_KEY_ACTION);
  566. case 8 -> List.of(KS_EVENT_SEVEN_DAY_RETENTION);
  567. case 9 -> List.of(KS_EVENT_THREE_DAY_RETENTION);
  568. case 2001 -> List.of(KS_EVENT_APP_WAKEUP);
  569. default -> List.of();
  570. };
  571. if (eventTypes.isEmpty()) {
  572. return List.of();
  573. }
  574. List<KuaishouTagEventRecord> records = new ArrayList<>(eventTypes.size());
  575. for (Integer eventType : eventTypes) {
  576. records.add(buildKuaishouTagEvent(tagId, baiduAct, eventType));
  577. }
  578. return records;
  579. }
  580. private List<VivoTagEventRecord> resolveConfiguredVivoTagEvents(String tagId, int baiduAct) {
  581. if (tagId == null || tagId.isBlank()) {
  582. return List.of();
  583. }
  584. try {
  585. if (vivoTagEventResolver != null) {
  586. List<String> eventTypes = vivoTagEventResolver.resolveEventTypes(tagId, baiduAct);
  587. if (eventTypes != null && !eventTypes.isEmpty()) {
  588. List<VivoTagEventRecord> records = new ArrayList<>(eventTypes.size());
  589. for (String eventType : eventTypes) {
  590. records.add(buildVivoTagEvent(tagId, baiduAct, eventType));
  591. }
  592. return records;
  593. }
  594. return List.of();
  595. }
  596. if (vivoColdStore == null) {
  597. return List.of();
  598. }
  599. List<VivoTagEventRecord> records = vivoColdStore.getTagEvents(tagId, baiduAct);
  600. if (records == null || records.isEmpty()) {
  601. records = vivoColdStore.getTagEvents(tagId, 0);
  602. }
  603. return records == null ? List.of() : records;
  604. } catch (Exception e) {
  605. log.warn("[ConversionSync] 查询 vivo tag event 失败 | tagId={} | act={} | error={}",
  606. tagId, baiduAct, e.getMessage(), e);
  607. return List.of();
  608. }
  609. }
  610. private static VivoTagEventRecord buildVivoTagEvent(String tagId, int baiduAct, String eventType) {
  611. VivoTagEventRecord record = new VivoTagEventRecord();
  612. record.setTagId(tagId);
  613. record.setBaiduAct(baiduAct);
  614. record.setEventType(eventType);
  615. return record;
  616. }
  617. private static List<VivoTagEventRecord> buildVivoFallbackTagEvents(String tagId, int baiduAct) {
  618. List<String> eventTypes = switch (baiduAct) {
  619. case 1 -> List.of(VIVO_EVENT_ACTIVATION);
  620. case 2, 5 -> List.of(VIVO_EVENT_PAY);
  621. case 3 -> List.of(VIVO_EVENT_REGISTER, VIVO_EVENT_ACTIVATION);
  622. case 4 -> List.of(VIVO_EVENT_RETENTION_1);
  623. case 6, 2001 -> List.of(VIVO_EVENT_REACTIVATION);
  624. case 7 -> List.of(VIVO_EVENT_OTHER);
  625. case 8 -> List.of(VIVO_EVENT_RETENTION_7);
  626. case 9 -> List.of(VIVO_EVENT_RETENTION_3);
  627. default -> List.of();
  628. };
  629. if (eventTypes.isEmpty()) {
  630. return List.of();
  631. }
  632. List<VivoTagEventRecord> records = new ArrayList<>(eventTypes.size());
  633. for (String eventType : eventTypes) {
  634. records.add(buildVivoTagEvent(tagId, baiduAct, eventType));
  635. }
  636. return records;
  637. }
  638. // ─── Step 3: 统一落百度转化明细 ───────────────────────────────────────────
  639. private void saveConversions(List<ConversionRecord> records) {
  640. if (coldStore == null || records.isEmpty()) {
  641. return;
  642. }
  643. try {
  644. coldStore.saveConversions(records);
  645. } catch (Exception e) {
  646. log.error("[ConversionSync] batch save conversions failed: size={}, {}", records.size(), e.getMessage(), e);
  647. }
  648. }
  649. // ─── Step 4: 回传前去重 ─────────────────────────────────────────────────
  650. private List<CallbackTask> filterPendingCallbacks(List<CallbackTask> callbackTasks, SyncResult result) {
  651. Set<String> existing = new HashSet<>();
  652. existing.addAll(collectTencentSuccessfulCallbackKeys(callbackTasks));
  653. existing.addAll(collectHonorSuccessfulCallbackKeys(callbackTasks));
  654. existing.addAll(collectKuaishouSuccessfulCallbackKeys(callbackTasks));
  655. existing.addAll(collectVivoSuccessfulCallbackKeys(callbackTasks));
  656. List<CallbackTask> pending = new ArrayList<>(callbackTasks.size());
  657. for (CallbackTask task : callbackTasks) {
  658. if (existing.contains(task.callbackKey)) {
  659. result.alreadySent.incrementAndGet();
  660. } else {
  661. pending.add(task);
  662. }
  663. }
  664. return pending;
  665. }
  666. private Set<String> collectTencentSuccessfulCallbackKeys(List<CallbackTask> callbackTasks) {
  667. if (coldStore == null) {
  668. return Set.of();
  669. }
  670. List<String> callbackKeys = new ArrayList<>();
  671. List<String> legacyKeys = new ArrayList<>();
  672. List<Integer> eventTypes = new ArrayList<>();
  673. for (CallbackTask task : callbackTasks) {
  674. if (!task.isTencent()) {
  675. continue;
  676. }
  677. callbackKeys.add(task.callbackKey);
  678. legacyKeys.add(legacyCallbackDedupeKey(task.payment, "tencent"));
  679. eventTypes.add(task.payment.getAct());
  680. }
  681. if (callbackKeys.isEmpty()) {
  682. return Set.of();
  683. }
  684. try {
  685. return new HashSet<>(coldStore.successfulCallbackKeysWithLegacy(callbackKeys, legacyKeys, eventTypes));
  686. } catch (Exception e) {
  687. log.error("[ConversionSync] batch dedup check error: size={}, {}", callbackKeys.size(), e.getMessage(), e);
  688. return Set.of();
  689. }
  690. }
  691. private Set<String> collectHonorSuccessfulCallbackKeys(List<CallbackTask> callbackTasks) {
  692. if (honorColdStore == null) {
  693. return Set.of();
  694. }
  695. Set<String> keys = new HashSet<>();
  696. for (CallbackTask task : callbackTasks) {
  697. if (task.isHonor()) {
  698. keys.add(task.callbackKey);
  699. }
  700. }
  701. if (keys.isEmpty()) {
  702. return Set.of();
  703. }
  704. try {
  705. return honorColdStore.successfulCallbackKeys(keys);
  706. } catch (Exception e) {
  707. log.error("[ConversionSync] honor dedup check error: size={}, {}", keys.size(), e.getMessage(), e);
  708. return Set.of();
  709. }
  710. }
  711. private Set<String> collectKuaishouSuccessfulCallbackKeys(List<CallbackTask> callbackTasks) {
  712. if (kuaishouColdStore == null) {
  713. return Set.of();
  714. }
  715. Set<String> keys = new HashSet<>();
  716. for (CallbackTask task : callbackTasks) {
  717. if (task.isKuaishou()) {
  718. keys.add(task.callbackKey);
  719. }
  720. }
  721. if (keys.isEmpty()) {
  722. return Set.of();
  723. }
  724. try {
  725. return kuaishouColdStore.successfulCallbackKeys(keys);
  726. } catch (Exception e) {
  727. log.error("[ConversionSync] kuaishou dedup check error: size={}, {}", keys.size(), e.getMessage(), e);
  728. return Set.of();
  729. }
  730. }
  731. private Set<String> collectVivoSuccessfulCallbackKeys(List<CallbackTask> callbackTasks) {
  732. if (vivoColdStore == null) {
  733. return Set.of();
  734. }
  735. Set<String> keys = new HashSet<>();
  736. for (CallbackTask task : callbackTasks) {
  737. if (task.isVivo()) {
  738. keys.add(task.callbackKey);
  739. }
  740. }
  741. if (keys.isEmpty()) {
  742. return Set.of();
  743. }
  744. try {
  745. return vivoColdStore.successfulCallbackKeys(keys);
  746. } catch (Exception e) {
  747. log.error("[ConversionSync] vivo dedup check error: size={}, {}", keys.size(), e.getMessage(), e);
  748. return Set.of();
  749. }
  750. }
  751. // ─── Step 5: 并发回传媒体 ───────────────────────────────────────────────
  752. private List<CallbackDispatchResult> dispatchCallbacks(List<CallbackTask> tasks,
  753. ExecutorService executor,
  754. SyncResult result) {
  755. List<CompletableFuture<CallbackDispatchResult>> futures = new ArrayList<>(tasks.size());
  756. for (CallbackTask task : tasks) {
  757. futures.add(CompletableFuture.supplyAsync(() -> sendCallback(task), executor));
  758. }
  759. List<CallbackDispatchResult> results = new ArrayList<>(tasks.size());
  760. for (CompletableFuture<CallbackDispatchResult> future : futures) {
  761. CallbackDispatchResult dispatchResult = future.join();
  762. if (dispatchResult == null || dispatchResult.record == null) {
  763. continue;
  764. }
  765. if (dispatchResult.record.isOk()) {
  766. result.sent.incrementAndGet();
  767. } else {
  768. result.failed.incrementAndGet();
  769. }
  770. results.add(dispatchResult);
  771. }
  772. return results;
  773. }
  774. private CallbackDispatchResult sendCallback(CallbackTask task) {
  775. if (task.isTencent()) {
  776. DeductionHandling handling = handleTencentDeduction(task);
  777. if (handling == DeductionHandling.SKIP_DUPLICATE) {
  778. return null;
  779. }
  780. if (handling == DeductionHandling.DEDUCT) {
  781. MediaCallbackRecord deductedRecord = buildDeductedCallbackRecord(task);
  782. return CallbackDispatchResult.tencent(task, deductedRecord);
  783. }
  784. String platform = resolvePlatform(task.tencentBid);
  785. MediaCallbackRecord callbackRecord = tencentClient.sendConversionEvent(
  786. task.tencentBid, task.payment, platform, task.tencentActionType, task.tencentCustomAction);
  787. callbackRecord.setDedupeKey(task.callbackKey);
  788. return CallbackDispatchResult.tencent(task, callbackRecord);
  789. }
  790. if (task.isHonor()) {
  791. HonorMediaCallbackRecord honorRecord = honorClient.sendConversion(
  792. task.honorBid, task.payment, task.honorConversionId);
  793. return CallbackDispatchResult.honor(task, toCallbackRecord(task, honorRecord));
  794. }
  795. if (task.isKuaishou()) {
  796. int deductionRate = resolveKuaishouDeductionRate(task.kuaishouBid);
  797. if (handleKuaishouDeduction(task)) {
  798. return CallbackDispatchResult.kuaishou(task, toCallbackRecord(task, buildDeductedKuaishouCallbackRecord(task, deductionRate)));
  799. }
  800. KuaishouMediaCallbackRecord kuaishouRecord = kuaishouClient.sendConversion(
  801. task.kuaishouBid, task.payment, task.kuaishouTagEvent, true);
  802. kuaishouRecord.setDeductionRate(deductionRate);
  803. return CallbackDispatchResult.kuaishou(task, toCallbackRecord(task, kuaishouRecord));
  804. }
  805. if (task.isVivo()) {
  806. int deductionRate = resolveVivoDeductionRate(task.vivoBid);
  807. if (handleVivoDeduction(task)) {
  808. return CallbackDispatchResult.vivo(task, toCallbackRecord(task, buildDeductedVivoCallbackRecord(task, deductionRate)));
  809. }
  810. VivoMediaCallbackRecord vivoRecord = vivoClient.sendConversion(
  811. task.vivoBid, task.payment, task.vivoTagEvent, true);
  812. vivoRecord.setDeductionRate(deductionRate);
  813. return CallbackDispatchResult.vivo(task, toCallbackRecord(task, vivoRecord));
  814. }
  815. return null;
  816. }
  817. // ─── Step 6: 落媒体回传结果 ─────────────────────────────────────────────
  818. private void saveCallbackResults(List<CallbackDispatchResult> results) {
  819. if (results.isEmpty()) {
  820. return;
  821. }
  822. List<MediaCallbackRecord> tencentRecords = new ArrayList<>();
  823. List<HonorMediaCallbackRecord> honorRecords = new ArrayList<>();
  824. List<KuaishouMediaCallbackRecord> kuaishouRecords = new ArrayList<>();
  825. List<VivoMediaCallbackRecord> vivoRecords = new ArrayList<>();
  826. for (CallbackDispatchResult result : results) {
  827. if (result.isHonor()) {
  828. honorRecords.add(toHonorCallbackRecord(result.task, result.record));
  829. } else if (result.isKuaishou()) {
  830. kuaishouRecords.add(toKuaishouCallbackRecord(result.task, result.record));
  831. } else if (result.isVivo()) {
  832. vivoRecords.add(toVivoCallbackRecord(result.task, result.record));
  833. } else {
  834. tencentRecords.add(result.record);
  835. }
  836. }
  837. if (coldStore != null && !tencentRecords.isEmpty()) {
  838. try {
  839. coldStore.saveMediaCallbacks(tencentRecords);
  840. } catch (Exception e) {
  841. log.error("[ConversionSync] batch save callbacks failed: size={}, {}", tencentRecords.size(), e.getMessage(), e);
  842. }
  843. }
  844. if (honorColdStore != null && !honorRecords.isEmpty()) {
  845. try {
  846. honorColdStore.saveMediaCallbacks(honorRecords);
  847. } catch (Exception e) {
  848. log.error("[ConversionSync] batch save honor callbacks failed: size={}, {}", honorRecords.size(), e.getMessage(), e);
  849. }
  850. }
  851. if (kuaishouColdStore != null && !kuaishouRecords.isEmpty()) {
  852. try {
  853. kuaishouColdStore.saveMediaCallbacks(kuaishouRecords);
  854. } catch (Exception e) {
  855. log.error("[ConversionSync] batch save kuaishou callbacks failed: size={}, {}", kuaishouRecords.size(), e.getMessage(), e);
  856. }
  857. }
  858. if (vivoColdStore != null && !vivoRecords.isEmpty()) {
  859. try {
  860. vivoColdStore.saveMediaCallbacks(vivoRecords);
  861. } catch (Exception e) {
  862. log.error("[ConversionSync] batch save vivo callbacks failed: size={}, {}", vivoRecords.size(), e.getMessage(), e);
  863. }
  864. }
  865. }
  866. // ─── 工具方法 ────────────────────────────────────────────────────────────
  867. private static String resolvePlatform(BidRecord bid) {
  868. if (bid.getPlatform() != null && !bid.getPlatform().isBlank()) {
  869. return bid.getPlatform().toLowerCase();
  870. }
  871. if (bid.getMediaParams() == null) {
  872. return "android";
  873. }
  874. String platform = bid.getMediaParams().get("baidu_platform");
  875. if (platform != null && !platform.isBlank()) {
  876. return platform.toLowerCase();
  877. }
  878. String idfa = bid.getMediaParams().get("idfa");
  879. if (idfa != null && !idfa.isBlank()) {
  880. return "ios";
  881. }
  882. return "android";
  883. }
  884. private static ConversionRecord toConversionRecord(PaymentInfo payment, BidRecord bid, String media) {
  885. ConversionRecord record = new ConversionRecord();
  886. record.setDedupeKey(conversionDedupeKey(payment));
  887. record.setQk(payment.getQk());
  888. record.setMedia(media);
  889. record.setTagId(bid != null ? bid.getTagId() : null);
  890. record.setDate(payment.getDate());
  891. record.setAppSid(payment.getAppSid());
  892. record.setCustomerName(payment.getCustomerName());
  893. record.setDeviceId(payment.getDeviceId());
  894. record.setConv(payment.getConv());
  895. record.setPayment(payment.getPayment());
  896. record.setGmv(payment.getGmv());
  897. record.setAct(payment.getAct());
  898. record.setTu(payment.getTu());
  899. record.setClkTime(payment.getClkTime());
  900. record.setCreatedAt(Instant.now());
  901. return record;
  902. }
  903. private static ConversionRecord toHonorConversionRecord(PaymentInfo payment, HonorBidRecord bid) {
  904. ConversionRecord record = new ConversionRecord();
  905. record.setDedupeKey(conversionDedupeKey(payment));
  906. record.setQk(payment.getQk());
  907. record.setMedia("honor");
  908. record.setTagId(bid != null ? bid.getTagId() : null);
  909. record.setDate(payment.getDate());
  910. record.setAppSid(payment.getAppSid());
  911. record.setCustomerName(payment.getCustomerName());
  912. record.setDeviceId(payment.getDeviceId());
  913. record.setConv(payment.getConv());
  914. record.setPayment(payment.getPayment());
  915. record.setGmv(payment.getGmv());
  916. record.setAct(payment.getAct());
  917. record.setTu(payment.getTu());
  918. record.setClkTime(payment.getClkTime());
  919. record.setCreatedAt(Instant.now());
  920. return record;
  921. }
  922. private static ConversionRecord toKuaishouConversionRecord(PaymentInfo payment, KuaishouBidRecord bid) {
  923. ConversionRecord record = new ConversionRecord();
  924. record.setDedupeKey(conversionDedupeKey(payment));
  925. record.setQk(payment.getQk());
  926. record.setMedia("kuaishou");
  927. record.setTagId(bid != null ? bid.getTagId() : null);
  928. record.setDate(payment.getDate());
  929. record.setAppSid(payment.getAppSid());
  930. record.setCustomerName(payment.getCustomerName());
  931. record.setDeviceId(payment.getDeviceId());
  932. record.setConv(payment.getConv());
  933. record.setPayment(payment.getPayment());
  934. record.setGmv(payment.getGmv());
  935. record.setAct(payment.getAct());
  936. record.setTu(payment.getTu());
  937. record.setClkTime(payment.getClkTime());
  938. record.setCreatedAt(Instant.now());
  939. return record;
  940. }
  941. private static ConversionRecord toVivoConversionRecord(PaymentInfo payment, VivoBidRecord bid) {
  942. ConversionRecord record = new ConversionRecord();
  943. record.setDedupeKey(conversionDedupeKey(payment));
  944. record.setQk(payment.getQk());
  945. record.setMedia("vivo");
  946. record.setTagId(bid != null ? bid.getTagId() : null);
  947. record.setDate(payment.getDate());
  948. record.setAppSid(payment.getAppSid());
  949. record.setCustomerName(payment.getCustomerName());
  950. record.setDeviceId(payment.getDeviceId());
  951. record.setConv(payment.getConv());
  952. record.setPayment(payment.getPayment());
  953. record.setGmv(payment.getGmv());
  954. record.setAct(payment.getAct());
  955. record.setTu(payment.getTu());
  956. record.setClkTime(payment.getClkTime());
  957. record.setCreatedAt(Instant.now());
  958. return record;
  959. }
  960. static String conversionDedupeKey(PaymentInfo payment) {
  961. return String.format("%s:%s:%s:%d",
  962. payment.getQk(),
  963. payment.getDate(),
  964. payment.getDeviceId(),
  965. payment.getAct());
  966. }
  967. static String callbackDedupeKey(PaymentInfo payment, BidRecord bid, String actionType) {
  968. return String.format("%s:%s:%s:%s:%s:%d:%s",
  969. bid.getMedia(),
  970. payment.getQk(),
  971. payment.getDate(),
  972. payment.getDeviceId(),
  973. bid.getTagId(),
  974. payment.getAct(),
  975. actionType);
  976. }
  977. static String legacyCallbackDedupeKey(PaymentInfo payment, String media) {
  978. return String.format("%s:%s:%s:%s:%d:%d",
  979. media,
  980. payment.getQk(),
  981. payment.getDate(),
  982. payment.getDeviceId(),
  983. payment.getAct(),
  984. payment.getAct());
  985. }
  986. static String honorCallbackDedupeKey(PaymentInfo payment, HonorBidRecord bid, int conversionId) {
  987. return String.format("honor:%s:%s:%s:%s:%d:%d",
  988. payment.getQk(),
  989. payment.getDate(),
  990. payment.getDeviceId(),
  991. bid.getTagId(),
  992. payment.getAct(),
  993. conversionId);
  994. }
  995. static String kuaishouCallbackDedupeKey(PaymentInfo payment, KuaishouBidRecord bid, int eventType) {
  996. return String.format("kuaishou:%s:%s:%s:%s:%d:%d",
  997. payment.getQk(),
  998. payment.getDate(),
  999. payment.getDeviceId(),
  1000. bid.getTagId(),
  1001. payment.getAct(),
  1002. eventType);
  1003. }
  1004. static String vivoCallbackDedupeKey(PaymentInfo payment, VivoBidRecord bid, String eventType) {
  1005. return String.format("vivo:%s:%s:%s:%s:%d:%s",
  1006. payment.getQk(),
  1007. payment.getDate(),
  1008. payment.getDeviceId(),
  1009. bid.getTagId(),
  1010. payment.getAct(),
  1011. eventType);
  1012. }
  1013. private DeductionHandling handleTencentDeduction(CallbackTask task) {
  1014. if (!task.isTencent() || task.tencentBid == null) {
  1015. return DeductionHandling.SEND;
  1016. }
  1017. int deductionRate = resolveDeductionRate(task.tencentBid, task.payment.getAct());
  1018. if (deductionRate <= 0) {
  1019. return DeductionHandling.SEND;
  1020. }
  1021. if (!isNewTrackingLink(task.tencentBid)) {
  1022. return DeductionHandling.SEND;
  1023. }
  1024. if (!hotStore.markConversionSeen(task.callbackKey)) {
  1025. log.info("[ConversionSync] 腾讯转化已见过,跳过后续发送 | callbackKey={} | qk={} | accountId={}",
  1026. task.callbackKey, task.payment.getQk(), safe(task.tencentBid.getAccountId()));
  1027. return DeductionHandling.SKIP_DUPLICATE;
  1028. }
  1029. RedisHotStore.DeductionDecision decision = hotStore.evaluateDeduction(
  1030. task.tencentBid.getAccountId(),
  1031. task.payment.getAct(),
  1032. resolveTrackingVersion(task.tencentBid),
  1033. task.payment.getDate(),
  1034. deductionRate
  1035. );
  1036. boolean deduct = decision.isShouldDeduct();
  1037. log.info("[ConversionSync] 腾讯转化扣量判定 | qk={} | act={} | accountId={} | version={} | rate={} | totalSeen={} | totalDeducted={} | callbackKey={} | deduct={}",
  1038. task.payment.getQk(), task.payment.getAct(), safe(task.tencentBid.getAccountId()),
  1039. resolveTrackingVersion(task.tencentBid), deductionRate,
  1040. decision.getTotalSeen(), decision.getTotalDeducted(), task.callbackKey, deduct);
  1041. return deduct ? DeductionHandling.DEDUCT : DeductionHandling.SEND;
  1042. }
  1043. private static boolean isNewTrackingLink(BidRecord bid) {
  1044. if (bid == null || bid.getMediaParams() == null) {
  1045. return false;
  1046. }
  1047. String flag = bid.getMediaParams().get(BidRecord.NEW_LINK_FLAG_KEY);
  1048. if (flag != null && Boolean.parseBoolean(flag)) {
  1049. return true;
  1050. }
  1051. String version = bid.getMediaParams().get(BidRecord.TRACKING_VERSION_KEY);
  1052. return "v2".equalsIgnoreCase(version)
  1053. || "v3".equalsIgnoreCase(version)
  1054. || "v4".equalsIgnoreCase(version);
  1055. }
  1056. private MediaCallbackRecord buildDeductedCallbackRecord(CallbackTask task) {
  1057. MediaCallbackRecord record = new MediaCallbackRecord();
  1058. Instant now = Instant.now();
  1059. record.setDedupeKey(task.callbackKey);
  1060. record.setMedia("tencent");
  1061. record.setQk(task.payment.getQk());
  1062. String callbackUrl = task.tencentBid.getMediaParams() != null
  1063. ? task.tencentBid.getMediaParams().get("callback")
  1064. : null;
  1065. record.setCallbackUrl(callbackUrl != null ? callbackUrl : "");
  1066. record.setEventType(task.payment.getAct());
  1067. record.setEventTimeMs(now.toEpochMilli());
  1068. record.setPurchase(task.payment.getPayment());
  1069. record.setStatus(0);
  1070. record.setOk(false);
  1071. record.setAttempt(1);
  1072. record.setErrorMessage("deducted_by_new_link_rate");
  1073. record.setDispatchStatus(MediaCallbackRecord.DISPATCH_STATUS_DEDUCTED);
  1074. record.setTrackingVersion(String.valueOf(resolveDeductionRate(task.tencentBid, task.payment.getAct())));
  1075. record.setCreatedAt(now);
  1076. int deductionRate = resolveDeductionRate(task.tencentBid, task.payment.getAct());
  1077. log.info("[ConversionSync] 腾讯转化扣量 | qk={} | act={} | rate={} | tagId={} | accountId={} | callbackKey={}",
  1078. task.payment.getQk(), task.payment.getAct(), deductionRate,
  1079. task.tencentBid.getTagId(), safe(task.tencentBid.getAccountId()), task.callbackKey);
  1080. return record;
  1081. }
  1082. private boolean handleKuaishouDeduction(CallbackTask task) {
  1083. if (!task.isKuaishou() || task.kuaishouBid == null || kuaishouHotStore == null) {
  1084. return false;
  1085. }
  1086. int deductionRate = resolveKuaishouDeductionRate(task.kuaishouBid);
  1087. if (deductionRate <= 0) {
  1088. return false;
  1089. }
  1090. if (!kuaishouHotStore.markConversionSeen(task.callbackKey)) {
  1091. log.info("[ConversionSync] 快手转化已见过,跳过重复扣量判断 | callbackKey={} | qk={} | accountId={}",
  1092. task.callbackKey, task.payment.getQk(), safe(task.kuaishouBid.getAccountId()));
  1093. return false;
  1094. }
  1095. KuaishouHotStore.DeductionDecision decision = kuaishouHotStore.evaluateDeduction(
  1096. task.kuaishouBid.getAccountId(),
  1097. task.payment.getAct(),
  1098. task.payment.getDate(),
  1099. deductionRate
  1100. );
  1101. boolean deduct = decision.isShouldDeduct();
  1102. log.info("[ConversionSync] 快手转化扣量判定 | qk={} | act={} | accountId={} | tagId={} | rate={} | totalSeen={} | totalDeducted={} | callbackKey={} | deduct={}",
  1103. task.payment.getQk(), task.payment.getAct(), safe(task.kuaishouBid.getAccountId()),
  1104. safe(task.kuaishouBid.getTagId()), deductionRate,
  1105. decision.getTotalSeen(), decision.getTotalDeducted(), task.callbackKey, deduct);
  1106. return deduct;
  1107. }
  1108. private KuaishouMediaCallbackRecord buildDeductedKuaishouCallbackRecord(CallbackTask task, int deductionRate) {
  1109. KuaishouMediaCallbackRecord record = new KuaishouMediaCallbackRecord();
  1110. Instant now = Instant.now();
  1111. record.setDedupeKey(task.callbackKey);
  1112. record.setQk(task.payment.getQk());
  1113. record.setTagId(task.kuaishouBid.getTagId());
  1114. String callbackUrl = task.kuaishouBid.getMediaParams() != null
  1115. ? task.kuaishouBid.getMediaParams().get(KuaishouBidRecord.CALLBACK_KEY)
  1116. : null;
  1117. record.setCallbackUrl(callbackUrl != null ? callbackUrl : "");
  1118. record.setEventType(task.kuaishouTagEvent.getEventType());
  1119. record.setEventTimeMs(now.toEpochMilli());
  1120. record.setPurchase(Math.max(task.payment.getPayment(), 0D));
  1121. record.setDeductionRate(deductionRate);
  1122. record.setDirectMatch(false);
  1123. record.setDispatchStatus(KuaishouMediaCallbackRecord.DISPATCH_STATUS_DEDUCTED);
  1124. record.setStatus(0);
  1125. record.setOk(false);
  1126. record.setAttempt(1);
  1127. record.setErrorMessage("deducted_by_rate");
  1128. record.setCreatedAt(now);
  1129. log.info("[ConversionSync] 快手转化扣量 | qk={} | act={} | rate={} | tagId={} | accountId={} | callbackKey={}",
  1130. task.payment.getQk(), task.payment.getAct(), deductionRate,
  1131. task.kuaishouBid.getTagId(), safe(task.kuaishouBid.getAccountId()), task.callbackKey);
  1132. return record;
  1133. }
  1134. private boolean handleVivoDeduction(CallbackTask task) {
  1135. if (!task.isVivo() || task.vivoBid == null || vivoHotStore == null) {
  1136. return false;
  1137. }
  1138. int deductionRate = resolveVivoDeductionRate(task.vivoBid);
  1139. if (deductionRate <= 0) {
  1140. return false;
  1141. }
  1142. if (!vivoHotStore.markConversionSeen(task.callbackKey)) {
  1143. log.info("[ConversionSync] vivo 转化已见过,跳过重复扣量判断 | callbackKey={} | qk={} | accountId={}",
  1144. task.callbackKey, task.payment.getQk(), safe(task.vivoBid.getAccountId()));
  1145. return false;
  1146. }
  1147. VivoHotStore.DeductionDecision decision = vivoHotStore.evaluateDeduction(
  1148. task.vivoBid.getAccountId(),
  1149. task.payment.getAct(),
  1150. task.payment.getDate(),
  1151. deductionRate
  1152. );
  1153. boolean deduct = decision.isShouldDeduct();
  1154. log.info("[ConversionSync] vivo 转化扣量判定 | qk={} | act={} | accountId={} | tagId={} | rate={} | totalSeen={} | totalDeducted={} | callbackKey={} | deduct={}",
  1155. task.payment.getQk(), task.payment.getAct(), safe(task.vivoBid.getAccountId()),
  1156. safe(task.vivoBid.getTagId()), deductionRate,
  1157. decision.getTotalSeen(), decision.getTotalDeducted(), task.callbackKey, deduct);
  1158. return deduct;
  1159. }
  1160. private VivoMediaCallbackRecord buildDeductedVivoCallbackRecord(CallbackTask task, int deductionRate) {
  1161. VivoMediaCallbackRecord record = new VivoMediaCallbackRecord();
  1162. Instant now = Instant.now();
  1163. record.setDedupeKey(task.callbackKey);
  1164. record.setQk(task.payment.getQk());
  1165. record.setTagId(task.vivoBid.getTagId());
  1166. record.setCallbackUrl("");
  1167. record.setEventType(task.vivoTagEvent.getEventType());
  1168. record.setEventTimeMs(now.toEpochMilli());
  1169. record.setPurchase(Math.max(task.payment.getPayment(), 0D));
  1170. record.setDeductionRate(deductionRate);
  1171. record.setDirectMatch(false);
  1172. record.setDispatchStatus(VivoMediaCallbackRecord.DISPATCH_STATUS_DEDUCTED);
  1173. record.setStatus(0);
  1174. record.setOk(false);
  1175. record.setAttempt(1);
  1176. record.setErrorMessage("deducted_by_rate");
  1177. record.setCreatedAt(now);
  1178. log.info("[ConversionSync] vivo 转化扣量 | qk={} | act={} | rate={} | tagId={} | accountId={} | callbackKey={}",
  1179. task.payment.getQk(), task.payment.getAct(), deductionRate,
  1180. task.vivoBid.getTagId(), safe(task.vivoBid.getAccountId()), task.callbackKey);
  1181. return record;
  1182. }
  1183. private static String resolveTrackingVersion(BidRecord bid) {
  1184. if (bid == null || bid.getMediaParams() == null) {
  1185. return "v1";
  1186. }
  1187. String version = bid.getMediaParams().get(BidRecord.TRACKING_VERSION_KEY);
  1188. return (version == null || version.isBlank()) ? "v1" : version;
  1189. }
  1190. private int resolveDeductionRate(BidRecord bid, int act) {
  1191. if (bid == null || bid.getMediaParams() == null) {
  1192. return 0;
  1193. }
  1194. if (act == 2001) {
  1195. String rawActRate = firstParam(bid.getMediaParams(),
  1196. BidRecord.ACT_2001_DEDUCTION_RATE_KEY,
  1197. "act2001DeductionRate",
  1198. "deductionRate2001",
  1199. "deduction_rate_2001");
  1200. Integer actRate = parseDeductionRate(rawActRate);
  1201. if (actRate != null) {
  1202. return actRate;
  1203. }
  1204. }
  1205. String rawRate = bid.getMediaParams().get(BidRecord.DEDUCTION_RATE_KEY);
  1206. Integer rate = parseDeductionRate(rawRate);
  1207. if (rate != null) {
  1208. return rate;
  1209. }
  1210. return 0;
  1211. }
  1212. private static Integer parseDeductionRate(String rawRate) {
  1213. if (rawRate == null || rawRate.isBlank()) {
  1214. return null;
  1215. }
  1216. try {
  1217. return sanitizeDeductionRate(Integer.parseInt(rawRate.trim()));
  1218. } catch (NumberFormatException ignored) {
  1219. return null;
  1220. }
  1221. }
  1222. private static String firstParam(Map<String, String> params, String... keys) {
  1223. for (String key : keys) {
  1224. String value = params.get(key);
  1225. if (value != null && !value.isBlank()) {
  1226. return value.trim();
  1227. }
  1228. }
  1229. return null;
  1230. }
  1231. private int resolveKuaishouDeductionRate(KuaishouBidRecord bid) {
  1232. if (bid == null || bid.getMediaParams() == null) {
  1233. return 0;
  1234. }
  1235. String rawRate = bid.getMediaParams().get(KuaishouBidRecord.DEDUCTION_RATE_KEY);
  1236. if (rawRate != null && !rawRate.isBlank()) {
  1237. try {
  1238. return sanitizeDeductionRate(Integer.parseInt(rawRate.trim()));
  1239. } catch (NumberFormatException ignored) {
  1240. }
  1241. }
  1242. return 0;
  1243. }
  1244. private int resolveVivoDeductionRate(VivoBidRecord bid) {
  1245. if (bid == null || bid.getMediaParams() == null) {
  1246. return 0;
  1247. }
  1248. String rawRate = bid.getMediaParams().get(VivoBidRecord.DEDUCTION_RATE_KEY);
  1249. if (rawRate != null && !rawRate.isBlank()) {
  1250. try {
  1251. return sanitizeDeductionRate(Integer.parseInt(rawRate.trim()));
  1252. } catch (NumberFormatException ignored) {
  1253. }
  1254. }
  1255. return 0;
  1256. }
  1257. private static String safe(String value) {
  1258. return value == null ? "" : value;
  1259. }
  1260. private static int sanitizeDeductionRate(int rate) {
  1261. if (rate < 0) {
  1262. return 0;
  1263. }
  1264. return Math.min(rate, 100);
  1265. }
  1266. private static HonorMediaCallbackRecord toHonorCallbackRecord(CallbackTask task, MediaCallbackRecord record) {
  1267. HonorMediaCallbackRecord callback = new HonorMediaCallbackRecord();
  1268. callback.setDedupeKey(record.getDedupeKey());
  1269. callback.setQk(record.getQk());
  1270. callback.setTagId(task.tagId);
  1271. callback.setCallbackUrl(record.getCallbackUrl());
  1272. callback.setEventType(record.getEventType());
  1273. callback.setEventTimeMs(record.getEventTimeMs());
  1274. callback.setPurchase(record.getPurchase());
  1275. callback.setStatus(record.getStatus());
  1276. callback.setOk(record.isOk());
  1277. callback.setAttempt(record.getAttempt());
  1278. callback.setResponseBody(record.getResponseBody());
  1279. callback.setErrorMessage(record.getErrorMessage());
  1280. callback.setRequestBody(record.getRequestBody());
  1281. callback.setCreatedAt(record.getCreatedAt());
  1282. return callback;
  1283. }
  1284. private static KuaishouMediaCallbackRecord toKuaishouCallbackRecord(CallbackTask task, MediaCallbackRecord record) {
  1285. KuaishouMediaCallbackRecord callback = new KuaishouMediaCallbackRecord();
  1286. callback.setDedupeKey(record.getDedupeKey());
  1287. callback.setQk(record.getQk());
  1288. callback.setTagId(task.tagId);
  1289. callback.setCallbackUrl(record.getCallbackUrl());
  1290. callback.setEventType(record.getEventType());
  1291. callback.setEventTimeMs(record.getEventTimeMs());
  1292. callback.setPurchase(record.getPurchase());
  1293. callback.setDeductionRate(parseKuaishouDeductionRate(task.kuaishouBid));
  1294. callback.setStatus(record.getStatus());
  1295. callback.setOk(record.isOk());
  1296. callback.setAttempt(record.getAttempt());
  1297. callback.setResponseBody(record.getResponseBody());
  1298. callback.setErrorMessage(record.getErrorMessage());
  1299. callback.setRequestBody(record.getRequestBody());
  1300. callback.setDispatchStatus(record.getDispatchStatus());
  1301. callback.setDirectMatch(!KuaishouMediaCallbackRecord.DISPATCH_STATUS_DEDUCTED.equals(record.getDispatchStatus()));
  1302. callback.setCreatedAt(record.getCreatedAt());
  1303. return callback;
  1304. }
  1305. private static VivoMediaCallbackRecord toVivoCallbackRecord(CallbackTask task, MediaCallbackRecord record) {
  1306. VivoMediaCallbackRecord callback = new VivoMediaCallbackRecord();
  1307. callback.setDedupeKey(record.getDedupeKey());
  1308. callback.setQk(record.getQk());
  1309. callback.setTagId(task.tagId);
  1310. callback.setCallbackUrl(record.getCallbackUrl());
  1311. callback.setEventType(record.getEventType() == 0 ? null : String.valueOf(record.getEventType()));
  1312. if (task.vivoTagEvent != null) {
  1313. callback.setEventType(task.vivoTagEvent.getEventType());
  1314. }
  1315. callback.setEventTimeMs(record.getEventTimeMs());
  1316. callback.setPurchase(record.getPurchase());
  1317. callback.setDeductionRate(parseVivoDeductionRate(task.vivoBid));
  1318. callback.setStatus(record.getStatus());
  1319. callback.setOk(record.isOk());
  1320. callback.setAttempt(record.getAttempt());
  1321. callback.setResponseBody(record.getResponseBody());
  1322. callback.setErrorMessage(record.getErrorMessage());
  1323. callback.setRequestBody(record.getRequestBody());
  1324. callback.setDispatchStatus(record.getDispatchStatus());
  1325. callback.setDirectMatch(!VivoMediaCallbackRecord.DISPATCH_STATUS_DEDUCTED.equals(record.getDispatchStatus()));
  1326. callback.setCreatedAt(record.getCreatedAt());
  1327. return callback;
  1328. }
  1329. private static MediaCallbackRecord toCallbackRecord(CallbackTask task, HonorMediaCallbackRecord honorRecord) {
  1330. MediaCallbackRecord callbackRecord = new MediaCallbackRecord();
  1331. callbackRecord.setDedupeKey(task.callbackKey);
  1332. callbackRecord.setMedia("honor");
  1333. callbackRecord.setQk(honorRecord.getQk());
  1334. callbackRecord.setCallbackUrl(honorRecord.getCallbackUrl());
  1335. callbackRecord.setEventType(honorRecord.getEventType());
  1336. callbackRecord.setEventTimeMs(honorRecord.getEventTimeMs());
  1337. callbackRecord.setPurchase(honorRecord.getPurchase());
  1338. callbackRecord.setStatus(honorRecord.getStatus());
  1339. callbackRecord.setOk(honorRecord.isOk());
  1340. callbackRecord.setAttempt(honorRecord.getAttempt());
  1341. callbackRecord.setResponseBody(honorRecord.getResponseBody());
  1342. callbackRecord.setErrorMessage(honorRecord.getErrorMessage());
  1343. callbackRecord.setRequestBody(honorRecord.getRequestBody());
  1344. callbackRecord.setTrackingVersion("v1");
  1345. callbackRecord.setCreatedAt(honorRecord.getCreatedAt());
  1346. return callbackRecord;
  1347. }
  1348. private static MediaCallbackRecord toCallbackRecord(CallbackTask task, KuaishouMediaCallbackRecord kuaishouRecord) {
  1349. MediaCallbackRecord callbackRecord = new MediaCallbackRecord();
  1350. callbackRecord.setDedupeKey(task.callbackKey);
  1351. callbackRecord.setMedia("kuaishou");
  1352. callbackRecord.setQk(kuaishouRecord.getQk());
  1353. callbackRecord.setCallbackUrl(kuaishouRecord.getCallbackUrl());
  1354. callbackRecord.setEventType(kuaishouRecord.getEventType());
  1355. callbackRecord.setEventTimeMs(kuaishouRecord.getEventTimeMs());
  1356. callbackRecord.setPurchase(kuaishouRecord.getPurchase());
  1357. callbackRecord.setStatus(kuaishouRecord.getStatus());
  1358. callbackRecord.setOk(kuaishouRecord.isOk());
  1359. callbackRecord.setAttempt(kuaishouRecord.getAttempt());
  1360. callbackRecord.setResponseBody(kuaishouRecord.getResponseBody());
  1361. callbackRecord.setErrorMessage(kuaishouRecord.getErrorMessage());
  1362. callbackRecord.setRequestBody(kuaishouRecord.getRequestBody());
  1363. callbackRecord.setDispatchStatus(kuaishouRecord.getDispatchStatus());
  1364. callbackRecord.setTrackingVersion(kuaishouRecord.isDirectMatch() ? "direct" : "deducted");
  1365. callbackRecord.setCreatedAt(kuaishouRecord.getCreatedAt());
  1366. return callbackRecord;
  1367. }
  1368. private static MediaCallbackRecord toCallbackRecord(CallbackTask task, VivoMediaCallbackRecord vivoRecord) {
  1369. MediaCallbackRecord callbackRecord = new MediaCallbackRecord();
  1370. callbackRecord.setDedupeKey(task.callbackKey);
  1371. callbackRecord.setMedia("vivo");
  1372. callbackRecord.setQk(vivoRecord.getQk());
  1373. callbackRecord.setCallbackUrl(vivoRecord.getCallbackUrl());
  1374. callbackRecord.setEventType(0);
  1375. callbackRecord.setEventTimeMs(vivoRecord.getEventTimeMs());
  1376. callbackRecord.setPurchase(vivoRecord.getPurchase());
  1377. callbackRecord.setStatus(vivoRecord.getStatus());
  1378. callbackRecord.setOk(vivoRecord.isOk());
  1379. callbackRecord.setAttempt(vivoRecord.getAttempt());
  1380. callbackRecord.setResponseBody(vivoRecord.getResponseBody());
  1381. callbackRecord.setErrorMessage(vivoRecord.getErrorMessage());
  1382. callbackRecord.setRequestBody(vivoRecord.getRequestBody());
  1383. callbackRecord.setDispatchStatus(vivoRecord.getDispatchStatus());
  1384. callbackRecord.setTrackingVersion(vivoRecord.isDirectMatch() ? "direct" : "deducted");
  1385. callbackRecord.setCreatedAt(vivoRecord.getCreatedAt());
  1386. return callbackRecord;
  1387. }
  1388. private static int parseKuaishouDeductionRate(KuaishouBidRecord bid) {
  1389. if (bid == null || bid.getMediaParams() == null) {
  1390. return 0;
  1391. }
  1392. String rawRate = bid.getMediaParams().get(KuaishouBidRecord.DEDUCTION_RATE_KEY);
  1393. if (rawRate == null || rawRate.isBlank()) {
  1394. return 0;
  1395. }
  1396. try {
  1397. int rate = Integer.parseInt(rawRate.trim());
  1398. if (rate < 0) {
  1399. return 0;
  1400. }
  1401. return Math.min(rate, 100);
  1402. } catch (NumberFormatException ignored) {
  1403. return 0;
  1404. }
  1405. }
  1406. private static int parseVivoDeductionRate(VivoBidRecord bid) {
  1407. if (bid == null || bid.getMediaParams() == null) {
  1408. return 0;
  1409. }
  1410. String rawRate = bid.getMediaParams().get(VivoBidRecord.DEDUCTION_RATE_KEY);
  1411. if (rawRate == null || rawRate.isBlank()) {
  1412. return 0;
  1413. }
  1414. try {
  1415. int rate = Integer.parseInt(rawRate.trim());
  1416. if (rate < 0) {
  1417. return 0;
  1418. }
  1419. return Math.min(rate, 100);
  1420. } catch (NumberFormatException ignored) {
  1421. return 0;
  1422. }
  1423. }
  1424. // ─── 内部数据结构 ────────────────────────────────────────────────────────
  1425. private static class BidLookup {
  1426. private final Map<String, BidRecord> tencentBids;
  1427. private final Map<String, HonorBidRecord> honorBids;
  1428. private final Map<String, KuaishouBidRecord> kuaishouBids;
  1429. private final Map<String, VivoBidRecord> vivoBids;
  1430. private BidLookup(Map<String, BidRecord> tencentBids,
  1431. Map<String, HonorBidRecord> honorBids,
  1432. Map<String, KuaishouBidRecord> kuaishouBids,
  1433. Map<String, VivoBidRecord> vivoBids) {
  1434. this.tencentBids = tencentBids;
  1435. this.honorBids = honorBids;
  1436. this.kuaishouBids = kuaishouBids;
  1437. this.vivoBids = vivoBids;
  1438. }
  1439. }
  1440. private static class CallbackTask {
  1441. private final String media;
  1442. private final PaymentInfo payment;
  1443. private final String tagId;
  1444. private final String callbackKey;
  1445. private final BidRecord tencentBid;
  1446. private final String tencentActionType;
  1447. private final String tencentCustomAction;
  1448. private final HonorBidRecord honorBid;
  1449. private final Integer honorConversionId;
  1450. private final KuaishouBidRecord kuaishouBid;
  1451. private final KuaishouTagEventRecord kuaishouTagEvent;
  1452. private final VivoBidRecord vivoBid;
  1453. private final VivoTagEventRecord vivoTagEvent;
  1454. private CallbackTask(String media,
  1455. PaymentInfo payment,
  1456. String tagId,
  1457. String callbackKey,
  1458. BidRecord tencentBid,
  1459. String tencentActionType,
  1460. String tencentCustomAction,
  1461. HonorBidRecord honorBid,
  1462. Integer honorConversionId,
  1463. KuaishouBidRecord kuaishouBid,
  1464. KuaishouTagEventRecord kuaishouTagEvent,
  1465. VivoBidRecord vivoBid,
  1466. VivoTagEventRecord vivoTagEvent) {
  1467. this.media = media;
  1468. this.payment = payment;
  1469. this.tagId = tagId;
  1470. this.callbackKey = callbackKey;
  1471. this.tencentBid = tencentBid;
  1472. this.tencentActionType = tencentActionType;
  1473. this.tencentCustomAction = tencentCustomAction;
  1474. this.honorBid = honorBid;
  1475. this.honorConversionId = honorConversionId;
  1476. this.kuaishouBid = kuaishouBid;
  1477. this.kuaishouTagEvent = kuaishouTagEvent;
  1478. this.vivoBid = vivoBid;
  1479. this.vivoTagEvent = vivoTagEvent;
  1480. }
  1481. private static CallbackTask tencent(PaymentInfo payment,
  1482. BidRecord bid,
  1483. String callbackKey,
  1484. String actionType,
  1485. String customAction) {
  1486. return new CallbackTask("tencent", payment, bid.getTagId(), callbackKey, bid, actionType, customAction, null, null, null, null, null, null);
  1487. }
  1488. private static CallbackTask honor(PaymentInfo payment,
  1489. HonorBidRecord bid,
  1490. String callbackKey,
  1491. Integer conversionId) {
  1492. return new CallbackTask("honor", payment, bid.getTagId(), callbackKey, null, null, null, bid, conversionId, null, null, null, null);
  1493. }
  1494. private static CallbackTask kuaishou(PaymentInfo payment,
  1495. KuaishouBidRecord bid,
  1496. String callbackKey,
  1497. KuaishouTagEventRecord tagEvent) {
  1498. return new CallbackTask("kuaishou", payment, bid.getTagId(), callbackKey, null, null, null, null, null, bid, tagEvent, null, null);
  1499. }
  1500. private static CallbackTask vivo(PaymentInfo payment,
  1501. VivoBidRecord bid,
  1502. String callbackKey,
  1503. VivoTagEventRecord tagEvent) {
  1504. return new CallbackTask("vivo", payment, bid.getTagId(), callbackKey, null, null, null, null, null, null, null, bid, tagEvent);
  1505. }
  1506. private boolean isTencent() {
  1507. return "tencent".equals(media);
  1508. }
  1509. private boolean isHonor() {
  1510. return "honor".equals(media);
  1511. }
  1512. private boolean isKuaishou() {
  1513. return "kuaishou".equals(media);
  1514. }
  1515. private boolean isVivo() {
  1516. return "vivo".equals(media);
  1517. }
  1518. }
  1519. private static class CallbackDispatchResult {
  1520. private final String media;
  1521. private final CallbackTask task;
  1522. private final MediaCallbackRecord record;
  1523. private CallbackDispatchResult(String media, CallbackTask task, MediaCallbackRecord record) {
  1524. this.media = media;
  1525. this.task = task;
  1526. this.record = record;
  1527. }
  1528. private static CallbackDispatchResult tencent(CallbackTask task, MediaCallbackRecord record) {
  1529. return new CallbackDispatchResult("tencent", task, record);
  1530. }
  1531. private static CallbackDispatchResult honor(CallbackTask task, MediaCallbackRecord record) {
  1532. return new CallbackDispatchResult("honor", task, record);
  1533. }
  1534. private static CallbackDispatchResult kuaishou(CallbackTask task, MediaCallbackRecord record) {
  1535. return new CallbackDispatchResult("kuaishou", task, record);
  1536. }
  1537. private static CallbackDispatchResult vivo(CallbackTask task, MediaCallbackRecord record) {
  1538. return new CallbackDispatchResult("vivo", task, record);
  1539. }
  1540. private boolean isHonor() {
  1541. return "honor".equals(media);
  1542. }
  1543. private boolean isKuaishou() {
  1544. return "kuaishou".equals(media);
  1545. }
  1546. private boolean isVivo() {
  1547. return "vivo".equals(media);
  1548. }
  1549. }
  1550. private enum DeductionHandling {
  1551. SEND,
  1552. DEDUCT,
  1553. SKIP_DUPLICATE
  1554. }
  1555. public static class SyncResult {
  1556. public final AtomicInteger fetched = new AtomicInteger();
  1557. public final AtomicInteger matched = new AtomicInteger();
  1558. public final AtomicInteger sent = new AtomicInteger();
  1559. public final AtomicInteger skipped = new AtomicInteger();
  1560. public final AtomicInteger failed = new AtomicInteger();
  1561. public final AtomicInteger alreadySent = new AtomicInteger();
  1562. }
  1563. }