ConversionSyncService.java 52 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196
  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.tagsync.TagEventResolver;
  26. import com.adx.tencent.tencent.TencentClient;
  27. import org.slf4j.Logger;
  28. import org.slf4j.LoggerFactory;
  29. import java.time.Instant;
  30. import java.util.ArrayList;
  31. import java.util.HashSet;
  32. import java.util.LinkedHashSet;
  33. import java.util.List;
  34. import java.util.Map;
  35. import java.util.Set;
  36. import java.util.concurrent.CompletableFuture;
  37. import java.util.concurrent.ExecutorService;
  38. import java.util.concurrent.Executors;
  39. import java.util.concurrent.TimeUnit;
  40. import java.util.concurrent.atomic.AtomicInteger;
  41. /**
  42. * 统一转化同步:
  43. * 1. 只从百度拉一次转化
  44. * 2. 按 qk 匹配腾讯/荣耀/快手 bid
  45. * 3. 百度转化明细统一落 tencent_baidu_conversions
  46. * 4. 媒体回传结果分别落各自表
  47. */
  48. public class ConversionSyncService {
  49. private static final Logger log = LoggerFactory.getLogger(ConversionSyncService.class);
  50. private static final int REALTIME_CONCURRENCY = 10;
  51. private static final int BACKFILL_CONCURRENCY = 16;
  52. private final ConversionClient baiduClient;
  53. private final RedisHotStore hotStore;
  54. private final HonorHotStore honorHotStore;
  55. private final KuaishouHotStore kuaishouHotStore;
  56. private final TencentClient tencentClient;
  57. private final TiDBColdStore coldStore;
  58. private final HonorColdStore honorColdStore;
  59. private final KuaishouColdStore kuaishouColdStore;
  60. private final HonorClient honorClient;
  61. private final KuaishouClient kuaishouClient;
  62. private final TagEventResolver tagEventResolver;
  63. private final HonorTagEventResolver honorTagEventResolver;
  64. private final KuaishouTagEventResolver kuaishouTagEventResolver;
  65. private final ExecutorService realtimeExecutor;
  66. private final ExecutorService backfillExecutor;
  67. public enum SyncMode {
  68. REALTIME,
  69. BACKFILL
  70. }
  71. public ConversionSyncService(ConversionClient baiduClient,
  72. RedisHotStore hotStore,
  73. TencentClient tencentClient,
  74. TiDBColdStore coldStore,
  75. TagEventResolver tagEventResolver,
  76. HonorTagEventResolver honorTagEventResolver,
  77. KuaishouTagEventResolver kuaishouTagEventResolver,
  78. HonorHotStore honorHotStore,
  79. HonorColdStore honorColdStore,
  80. KuaishouHotStore kuaishouHotStore,
  81. KuaishouColdStore kuaishouColdStore,
  82. HonorClient honorClient,
  83. KuaishouClient kuaishouClient,
  84. AppProperties props) {
  85. this.baiduClient = baiduClient;
  86. this.hotStore = hotStore;
  87. this.tencentClient = tencentClient;
  88. this.coldStore = coldStore;
  89. this.tagEventResolver = tagEventResolver;
  90. this.honorTagEventResolver = honorTagEventResolver;
  91. this.kuaishouTagEventResolver = kuaishouTagEventResolver;
  92. this.honorHotStore = honorHotStore;
  93. this.honorColdStore = honorColdStore;
  94. this.kuaishouHotStore = kuaishouHotStore;
  95. this.kuaishouColdStore = kuaishouColdStore;
  96. this.honorClient = honorClient;
  97. this.kuaishouClient = kuaishouClient;
  98. this.realtimeExecutor = Executors.newFixedThreadPool(
  99. REALTIME_CONCURRENCY,
  100. r -> {
  101. Thread t = new Thread(r, "conv-sync-worker");
  102. t.setDaemon(true);
  103. return t;
  104. });
  105. this.backfillExecutor = Executors.newFixedThreadPool(
  106. BACKFILL_CONCURRENCY,
  107. r -> {
  108. Thread t = new Thread(r, "conv-backfill-worker");
  109. t.setDaemon(true);
  110. return t;
  111. });
  112. }
  113. public SyncResult syncTencentConversions(ConversionQuery query) throws Exception {
  114. return syncTencentConversions(query, SyncMode.REALTIME);
  115. }
  116. public SyncResult syncTencentConversions(ConversionQuery query, SyncMode mode) throws Exception {
  117. if (query.getPageSize() <= 0) {
  118. query.setPageSize(1);
  119. }
  120. SyncResult result = new SyncResult();
  121. int maxPages = 10000;
  122. for (int page = query.getPageSize(); ; page++) {
  123. query.setPageSize(page);
  124. long pageStartNs = System.nanoTime();
  125. log.info("[ConversionSync] fetching date={}, page={}, mode={}", query.getDate(), page, mode);
  126. ConversionResponse response = baiduClient.queryPayments(query);
  127. List<PaymentInfo> data = response.getData();
  128. int dataSize = data != null ? data.size() : 0;
  129. log.info("[ConversionSync] date={}, page={} returned {} records, responsePageSize={}, mode={}",
  130. query.getDate(), page, dataSize, response.getPageSize(), mode);
  131. if (data != null) {
  132. processPayments(data, result, mode);
  133. }
  134. long pageCostMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - pageStartNs);
  135. log.info("[ConversionSync] date={}, page={} processed, cost={}ms, totalFetched={}, matched={}, sent={}, failed={}, skipped={}, alreadySent={}, mode={}",
  136. query.getDate(), page, pageCostMs,
  137. result.fetched.get(), result.matched.get(), result.sent.get(), result.failed.get(),
  138. result.skipped.get(), result.alreadySent.get(), mode);
  139. if (response.getPageSize() <= page) {
  140. break;
  141. }
  142. if (page >= maxPages) {
  143. log.warn("[ConversionSync] reached max page limit ({}), stopping", maxPages);
  144. break;
  145. }
  146. }
  147. return result;
  148. }
  149. private void processPayments(List<PaymentInfo> payments, SyncResult result, SyncMode mode) {
  150. if (payments == null || payments.isEmpty()) {
  151. return;
  152. }
  153. ExecutorService executor = mode == SyncMode.BACKFILL ? backfillExecutor : realtimeExecutor;
  154. // 1. 批量查这一页转化对应的腾讯/荣耀/快手 bid;Redis miss 时会自动回落到冷库。
  155. BidLookup lookup = loadBidLookup(payments, result);
  156. // 2. 统一整理要落库的百度转化明细,以及要回传给媒体的任务。
  157. List<ConversionRecord> conversionRecords = new ArrayList<>();
  158. List<CallbackTask> callbackTasks = new ArrayList<>();
  159. buildRecordsAndTasks(payments, lookup, conversionRecords, callbackTasks, result);
  160. // 3. 百度转化明细统一落 tencent_baidu_conversions。
  161. saveConversions(conversionRecords);
  162. if (callbackTasks.isEmpty()) {
  163. return;
  164. }
  165. // 4. 回传前先做去重,只发送未成功回传过的任务。
  166. List<CallbackTask> pendingTasks = filterPendingCallbacks(callbackTasks, result);
  167. if (pendingTasks.isEmpty()) {
  168. return;
  169. }
  170. // 5. 并发回传媒体,并把回传结果分别落到腾讯/荣耀回调表。
  171. List<CallbackDispatchResult> dispatchResults = dispatchCallbacks(pendingTasks, executor, result);
  172. saveCallbackResults(dispatchResults);
  173. }
  174. // ─── Step 1: 加载 bid ───────────────────────────────────────────────────
  175. private BidLookup loadBidLookup(List<PaymentInfo> payments, SyncResult result) {
  176. Set<String> qks = new LinkedHashSet<>();
  177. for (PaymentInfo payment : payments) {
  178. result.fetched.incrementAndGet();
  179. if (payment.getQk() != null && !payment.getQk().isBlank()) {
  180. qks.add(payment.getQk());
  181. }
  182. }
  183. List<String> qkList = new ArrayList<>(qks);
  184. Map<String, BidRecord> tencentBids = hotStore.findBidsByQks(qkList);
  185. backfillTencentAccountIds(qkList, tencentBids);
  186. Map<String, HonorBidRecord> honorBids = honorHotStore != null
  187. ? honorHotStore.findBidsByQks(qkList)
  188. : Map.of();
  189. Map<String, KuaishouBidRecord> kuaishouBids = kuaishouHotStore != null
  190. ? kuaishouHotStore.findBidsByQks(qkList)
  191. : Map.of();
  192. return new BidLookup(tencentBids, honorBids, kuaishouBids);
  193. }
  194. private void backfillTencentAccountIds(List<String> qkList, Map<String, BidRecord> tencentBids) {
  195. if (coldStore == null || qkList == null || qkList.isEmpty() || tencentBids == null || tencentBids.isEmpty()) {
  196. return;
  197. }
  198. List<String> missingAccountQks = new ArrayList<>();
  199. for (String qk : qkList) {
  200. BidRecord bid = tencentBids.get(qk);
  201. if (bid != null
  202. && "tencent".equals(bid.getMedia())
  203. && (bid.getAccountId() == null || bid.getAccountId().isBlank())) {
  204. missingAccountQks.add(qk);
  205. }
  206. }
  207. if (missingAccountQks.isEmpty()) {
  208. return;
  209. }
  210. try {
  211. for (BidRecord coldBid : coldStore.getBidsByQks(missingAccountQks)) {
  212. if (coldBid == null || coldBid.getQk() == null) {
  213. continue;
  214. }
  215. BidRecord hotBid = tencentBids.get(coldBid.getQk());
  216. if (hotBid == null || hotBid.getAccountId() != null && !hotBid.getAccountId().isBlank()) {
  217. continue;
  218. }
  219. if (coldBid.getAccountId() == null || coldBid.getAccountId().isBlank()) {
  220. continue;
  221. }
  222. hotBid.setAccountId(coldBid.getAccountId());
  223. log.info("[ConversionSync] 腾讯 bid accountId 回落冷库补齐 | qk={} | accountId={}",
  224. coldBid.getQk(), coldBid.getAccountId());
  225. }
  226. } catch (Exception e) {
  227. log.warn("[ConversionSync] 腾讯 bid accountId 回落冷库失败 | size={} | error={}",
  228. missingAccountQks.size(), e.getMessage(), e);
  229. }
  230. }
  231. // ─── Step 2: 组装转化记录与回传任务 ───────────────────────────────────────
  232. private void buildRecordsAndTasks(List<PaymentInfo> payments,
  233. BidLookup lookup,
  234. List<ConversionRecord> conversionRecords,
  235. List<CallbackTask> callbackTasks,
  236. SyncResult result) {
  237. for (PaymentInfo payment : payments) {
  238. BidRecord tencentBid = lookup.tencentBids.get(payment.getQk());
  239. HonorBidRecord honorBid = lookup.honorBids.get(payment.getQk());
  240. KuaishouBidRecord kuaishouBid = lookup.kuaishouBids.get(payment.getQk());
  241. if (tencentBid == null && honorBid == null && kuaishouBid == null) {
  242. conversionRecords.add(toConversionRecord(payment, null, ""));
  243. result.skipped.incrementAndGet();
  244. continue;
  245. }
  246. boolean hasTencentBid = tencentBid != null;
  247. boolean hasHonorBid = honorBid != null;
  248. boolean hasKuaishouBid = kuaishouBid != null;
  249. boolean validTencent = hasTencentBid && "tencent".equals(tencentBid.getMedia());
  250. boolean validHonor = hasHonorBid && "honor".equals(honorBid.getMedia());
  251. boolean validKuaishou = hasKuaishouBid && "kuaishou".equals(kuaishouBid.getMedia());
  252. if (hasTencentBid && !validTencent) {
  253. log.warn("[ConversionSync] 腾讯 bid media 非法 | qk={} | media={}", payment.getQk(), tencentBid.getMedia());
  254. }
  255. if (hasHonorBid && !validHonor) {
  256. log.warn("[ConversionSync] 荣耀 bid media 非法 | qk={} | media={}", payment.getQk(), honorBid.getMedia());
  257. }
  258. if (hasKuaishouBid && !validKuaishou) {
  259. log.warn("[ConversionSync] 快手 bid media 非法 | qk={} | media={}", payment.getQk(), kuaishouBid.getMedia());
  260. }
  261. int matchedMediaCount = (validTencent ? 1 : 0) + (validHonor ? 1 : 0) + (validKuaishou ? 1 : 0);
  262. if (matchedMediaCount > 1) {
  263. log.warn("[ConversionSync] qk 同时命中多媒体,跳过分发 | qk={} | act={} | date={}",
  264. payment.getQk(), payment.getAct(), payment.getDate());
  265. result.skipped.incrementAndGet();
  266. continue;
  267. }
  268. if (validTencent) {
  269. conversionRecords.add(toConversionRecord(payment, tencentBid, "tencent"));
  270. result.matched.incrementAndGet();
  271. log.info("[ConversionSync] 匹配到腾讯转化 | qk={} | act={} | deviceId={} | date={} | payment={}",
  272. payment.getQk(), payment.getAct(), payment.getDeviceId(), payment.getDate(), payment.getPayment());
  273. CallbackTask tencentTask = buildTencentCallbackTask(payment, tencentBid);
  274. if (tencentTask != null) {
  275. callbackTasks.add(tencentTask);
  276. }
  277. continue;
  278. }
  279. if (!validHonor) {
  280. if (validKuaishou) {
  281. conversionRecords.add(toKuaishouConversionRecord(payment, kuaishouBid));
  282. result.matched.incrementAndGet();
  283. log.info("[ConversionSync] 匹配到快手转化 | qk={} | act={} | deviceId={} | date={} | payment={}",
  284. payment.getQk(), payment.getAct(), payment.getDeviceId(), payment.getDate(), payment.getPayment());
  285. CallbackTask kuaishouTask = buildKuaishouCallbackTask(payment, kuaishouBid);
  286. if (kuaishouTask != null) {
  287. callbackTasks.add(kuaishouTask);
  288. }
  289. continue;
  290. }
  291. conversionRecords.add(toConversionRecord(payment, hasTencentBid ? tencentBid : null, hasTencentBid ? tencentBid.getMedia() : ""));
  292. result.skipped.incrementAndGet();
  293. continue;
  294. }
  295. conversionRecords.add(toHonorConversionRecord(payment, honorBid));
  296. result.matched.incrementAndGet();
  297. log.info("[ConversionSync] 匹配到荣耀转化 | qk={} | act={} | deviceId={} | date={} | payment={}",
  298. payment.getQk(), payment.getAct(), payment.getDeviceId(), payment.getDate(), payment.getPayment());
  299. CallbackTask honorTask = buildHonorCallbackTask(payment, honorBid);
  300. if (honorTask != null) {
  301. callbackTasks.add(honorTask);
  302. }
  303. }
  304. }
  305. private CallbackTask buildTencentCallbackTask(PaymentInfo payment, BidRecord bid) {
  306. String actionType = null;
  307. if (tagEventResolver != null) {
  308. actionType = tagEventResolver.resolveActionType(bid.getTagId(), payment.getAct());
  309. }
  310. if (actionType == null || actionType.isBlank()) {
  311. try {
  312. actionType = tencentClient.conversionActionType(payment.getAct());
  313. log.warn("[ConversionSync] 广告位回传方式未配置, 回退act-map | qk={} | tagId={} | act={} | actionType={}",
  314. payment.getQk(), bid.getTagId(), payment.getAct(), actionType);
  315. } catch (IllegalArgumentException e) {
  316. // act 既没配广告位回传方式,也没配 act-map:只落库不回传,避免整个任务中断
  317. log.warn("[ConversionSync] 腾讯回传方式未配置, 跳过回传 | qk={} | tagId={} | act={} | date={} | error={}",
  318. payment.getQk(), bid.getTagId(), payment.getAct(), payment.getDate(), e.getMessage());
  319. return null;
  320. }
  321. }
  322. return CallbackTask.tencent(
  323. payment,
  324. bid,
  325. callbackDedupeKey(payment, bid, actionType),
  326. actionType
  327. );
  328. }
  329. private CallbackTask buildHonorCallbackTask(PaymentInfo payment, HonorBidRecord bid) {
  330. if (honorColdStore == null && honorTagEventResolver == null) {
  331. return null;
  332. }
  333. Integer conversionId = honorTagEventResolver != null
  334. ? honorTagEventResolver.resolveConversionId(bid.getTagId(), payment.getAct())
  335. : resolveHonorConversionIdFromDb(bid.getTagId(), payment.getAct());
  336. if (conversionId == null) {
  337. log.warn("[ConversionSync] 荣耀 conversionId 未配置 | qk={} | tagId={} | act={}",
  338. payment.getQk(), bid.getTagId(), payment.getAct());
  339. return null;
  340. }
  341. return CallbackTask.honor(
  342. payment,
  343. bid,
  344. honorCallbackDedupeKey(payment, bid, conversionId),
  345. conversionId
  346. );
  347. }
  348. private CallbackTask buildKuaishouCallbackTask(PaymentInfo payment, KuaishouBidRecord bid) {
  349. if (kuaishouColdStore == null && kuaishouTagEventResolver == null) {
  350. return null;
  351. }
  352. KuaishouTagEventRecord tagEvent = resolveKuaishouTagEvent(bid.getTagId(), payment.getAct());
  353. if (tagEvent == null) {
  354. log.warn("[ConversionSync] 快手 eventType 未配置 | qk={} | tagId={} | act={}",
  355. payment.getQk(), bid.getTagId(), payment.getAct());
  356. return null;
  357. }
  358. return CallbackTask.kuaishou(
  359. payment,
  360. bid,
  361. kuaishouCallbackDedupeKey(payment, bid, tagEvent.getEventType()),
  362. tagEvent
  363. );
  364. }
  365. private Integer resolveHonorConversionIdFromDb(String tagId, int baiduAct) {
  366. if (honorColdStore == null) {
  367. return null;
  368. }
  369. com.adx.tencent.honor.model.HonorTagEventRecord exact = honorColdStore.getTagEvent(tagId, baiduAct);
  370. if (exact != null) {
  371. return exact.getConversionId();
  372. }
  373. com.adx.tencent.honor.model.HonorTagEventRecord fallback = honorColdStore.getTagEvent(tagId, 0);
  374. return fallback != null ? fallback.getConversionId() : null;
  375. }
  376. private KuaishouTagEventRecord resolveKuaishouTagEvent(String tagId, int baiduAct) {
  377. if (tagId == null || tagId.isBlank() || kuaishouColdStore == null) {
  378. return null;
  379. }
  380. try {
  381. KuaishouTagEventRecord record = kuaishouColdStore.getTagEvent(tagId, baiduAct);
  382. if (record == null) {
  383. record = kuaishouColdStore.getTagEvent(tagId, 0);
  384. }
  385. return record;
  386. } catch (Exception e) {
  387. log.warn("[ConversionSync] 查询快手 tag event 失败 | tagId={} | act={} | error={}",
  388. tagId, baiduAct, e.getMessage(), e);
  389. return null;
  390. }
  391. }
  392. // ─── Step 3: 统一落百度转化明细 ───────────────────────────────────────────
  393. private void saveConversions(List<ConversionRecord> records) {
  394. if (coldStore == null || records.isEmpty()) {
  395. return;
  396. }
  397. try {
  398. coldStore.saveConversions(records);
  399. } catch (Exception e) {
  400. log.error("[ConversionSync] batch save conversions failed: size={}, {}", records.size(), e.getMessage(), e);
  401. }
  402. }
  403. // ─── Step 4: 回传前去重 ─────────────────────────────────────────────────
  404. private List<CallbackTask> filterPendingCallbacks(List<CallbackTask> callbackTasks, SyncResult result) {
  405. Set<String> existing = new HashSet<>();
  406. existing.addAll(collectTencentSuccessfulCallbackKeys(callbackTasks));
  407. existing.addAll(collectHonorSuccessfulCallbackKeys(callbackTasks));
  408. existing.addAll(collectKuaishouSuccessfulCallbackKeys(callbackTasks));
  409. List<CallbackTask> pending = new ArrayList<>(callbackTasks.size());
  410. for (CallbackTask task : callbackTasks) {
  411. if (existing.contains(task.callbackKey)) {
  412. result.alreadySent.incrementAndGet();
  413. } else {
  414. pending.add(task);
  415. }
  416. }
  417. return pending;
  418. }
  419. private Set<String> collectTencentSuccessfulCallbackKeys(List<CallbackTask> callbackTasks) {
  420. if (coldStore == null) {
  421. return Set.of();
  422. }
  423. List<String> callbackKeys = new ArrayList<>();
  424. List<String> legacyKeys = new ArrayList<>();
  425. List<Integer> eventTypes = new ArrayList<>();
  426. for (CallbackTask task : callbackTasks) {
  427. if (!task.isTencent()) {
  428. continue;
  429. }
  430. callbackKeys.add(task.callbackKey);
  431. legacyKeys.add(legacyCallbackDedupeKey(task.payment, "tencent"));
  432. eventTypes.add(task.payment.getAct());
  433. }
  434. if (callbackKeys.isEmpty()) {
  435. return Set.of();
  436. }
  437. try {
  438. return new HashSet<>(coldStore.successfulCallbackKeysWithLegacy(callbackKeys, legacyKeys, eventTypes));
  439. } catch (Exception e) {
  440. log.error("[ConversionSync] batch dedup check error: size={}, {}", callbackKeys.size(), e.getMessage(), e);
  441. return Set.of();
  442. }
  443. }
  444. private Set<String> collectHonorSuccessfulCallbackKeys(List<CallbackTask> callbackTasks) {
  445. if (honorColdStore == null) {
  446. return Set.of();
  447. }
  448. Set<String> keys = new HashSet<>();
  449. for (CallbackTask task : callbackTasks) {
  450. if (task.isHonor()) {
  451. keys.add(task.callbackKey);
  452. }
  453. }
  454. if (keys.isEmpty()) {
  455. return Set.of();
  456. }
  457. try {
  458. return honorColdStore.successfulCallbackKeys(keys);
  459. } catch (Exception e) {
  460. log.error("[ConversionSync] honor dedup check error: size={}, {}", keys.size(), e.getMessage(), e);
  461. return Set.of();
  462. }
  463. }
  464. private Set<String> collectKuaishouSuccessfulCallbackKeys(List<CallbackTask> callbackTasks) {
  465. if (kuaishouColdStore == null) {
  466. return Set.of();
  467. }
  468. Set<String> keys = new HashSet<>();
  469. for (CallbackTask task : callbackTasks) {
  470. if (task.isKuaishou()) {
  471. keys.add(task.callbackKey);
  472. }
  473. }
  474. if (keys.isEmpty()) {
  475. return Set.of();
  476. }
  477. try {
  478. return kuaishouColdStore.successfulCallbackKeys(keys);
  479. } catch (Exception e) {
  480. log.error("[ConversionSync] kuaishou dedup check error: size={}, {}", keys.size(), e.getMessage(), e);
  481. return Set.of();
  482. }
  483. }
  484. // ─── Step 5: 并发回传媒体 ───────────────────────────────────────────────
  485. private List<CallbackDispatchResult> dispatchCallbacks(List<CallbackTask> tasks,
  486. ExecutorService executor,
  487. SyncResult result) {
  488. List<CompletableFuture<CallbackDispatchResult>> futures = new ArrayList<>(tasks.size());
  489. for (CallbackTask task : tasks) {
  490. futures.add(CompletableFuture.supplyAsync(() -> sendCallback(task), executor));
  491. }
  492. List<CallbackDispatchResult> results = new ArrayList<>(tasks.size());
  493. for (CompletableFuture<CallbackDispatchResult> future : futures) {
  494. CallbackDispatchResult dispatchResult = future.join();
  495. if (dispatchResult == null || dispatchResult.record == null) {
  496. continue;
  497. }
  498. if (dispatchResult.record.isOk()) {
  499. result.sent.incrementAndGet();
  500. } else {
  501. result.failed.incrementAndGet();
  502. }
  503. results.add(dispatchResult);
  504. }
  505. return results;
  506. }
  507. private CallbackDispatchResult sendCallback(CallbackTask task) {
  508. if (task.isTencent()) {
  509. DeductionHandling handling = handleTencentDeduction(task);
  510. if (handling == DeductionHandling.SKIP_DUPLICATE) {
  511. return null;
  512. }
  513. if (handling == DeductionHandling.DEDUCT) {
  514. MediaCallbackRecord deductedRecord = buildDeductedCallbackRecord(task);
  515. return CallbackDispatchResult.tencent(task, deductedRecord);
  516. }
  517. String platform = resolvePlatform(task.tencentBid);
  518. MediaCallbackRecord callbackRecord = tencentClient.sendConversionEvent(
  519. task.tencentBid, task.payment, platform, task.tencentActionType);
  520. callbackRecord.setDedupeKey(task.callbackKey);
  521. return CallbackDispatchResult.tencent(task, callbackRecord);
  522. }
  523. if (task.isHonor()) {
  524. HonorMediaCallbackRecord honorRecord = honorClient.sendConversion(
  525. task.honorBid, task.payment, task.honorConversionId);
  526. return CallbackDispatchResult.honor(task, toCallbackRecord(task, honorRecord));
  527. }
  528. if (task.isKuaishou()) {
  529. int deductionRate = resolveKuaishouDeductionRate(task.kuaishouBid);
  530. if (handleKuaishouDeduction(task)) {
  531. return CallbackDispatchResult.kuaishou(task, toCallbackRecord(task, buildDeductedKuaishouCallbackRecord(task, deductionRate)));
  532. }
  533. KuaishouMediaCallbackRecord kuaishouRecord = kuaishouClient.sendConversion(
  534. task.kuaishouBid, task.payment, task.kuaishouTagEvent, true);
  535. kuaishouRecord.setDeductionRate(deductionRate);
  536. return CallbackDispatchResult.kuaishou(task, toCallbackRecord(task, kuaishouRecord));
  537. }
  538. return null;
  539. }
  540. // ─── Step 6: 落媒体回传结果 ─────────────────────────────────────────────
  541. private void saveCallbackResults(List<CallbackDispatchResult> results) {
  542. if (results.isEmpty()) {
  543. return;
  544. }
  545. List<MediaCallbackRecord> tencentRecords = new ArrayList<>();
  546. List<HonorMediaCallbackRecord> honorRecords = new ArrayList<>();
  547. List<KuaishouMediaCallbackRecord> kuaishouRecords = new ArrayList<>();
  548. for (CallbackDispatchResult result : results) {
  549. if (result.isHonor()) {
  550. honorRecords.add(toHonorCallbackRecord(result.task, result.record));
  551. } else if (result.isKuaishou()) {
  552. kuaishouRecords.add(toKuaishouCallbackRecord(result.task, result.record));
  553. } else {
  554. tencentRecords.add(result.record);
  555. }
  556. }
  557. if (coldStore != null && !tencentRecords.isEmpty()) {
  558. try {
  559. coldStore.saveMediaCallbacks(tencentRecords);
  560. } catch (Exception e) {
  561. log.error("[ConversionSync] batch save callbacks failed: size={}, {}", tencentRecords.size(), e.getMessage(), e);
  562. }
  563. }
  564. if (honorColdStore != null && !honorRecords.isEmpty()) {
  565. try {
  566. honorColdStore.saveMediaCallbacks(honorRecords);
  567. } catch (Exception e) {
  568. log.error("[ConversionSync] batch save honor callbacks failed: size={}, {}", honorRecords.size(), e.getMessage(), e);
  569. }
  570. }
  571. if (kuaishouColdStore != null && !kuaishouRecords.isEmpty()) {
  572. try {
  573. kuaishouColdStore.saveMediaCallbacks(kuaishouRecords);
  574. } catch (Exception e) {
  575. log.error("[ConversionSync] batch save kuaishou callbacks failed: size={}, {}", kuaishouRecords.size(), e.getMessage(), e);
  576. }
  577. }
  578. }
  579. // ─── 工具方法 ────────────────────────────────────────────────────────────
  580. private static String resolvePlatform(BidRecord bid) {
  581. if (bid.getPlatform() != null && !bid.getPlatform().isBlank()) {
  582. return bid.getPlatform().toLowerCase();
  583. }
  584. if (bid.getMediaParams() == null) {
  585. return "android";
  586. }
  587. String platform = bid.getMediaParams().get("baidu_platform");
  588. if (platform != null && !platform.isBlank()) {
  589. return platform.toLowerCase();
  590. }
  591. String idfa = bid.getMediaParams().get("idfa");
  592. if (idfa != null && !idfa.isBlank()) {
  593. return "ios";
  594. }
  595. return "android";
  596. }
  597. private static ConversionRecord toConversionRecord(PaymentInfo payment, BidRecord bid, String media) {
  598. ConversionRecord record = new ConversionRecord();
  599. record.setDedupeKey(conversionDedupeKey(payment));
  600. record.setQk(payment.getQk());
  601. record.setMedia(media);
  602. record.setTagId(bid != null ? bid.getTagId() : null);
  603. record.setDate(payment.getDate());
  604. record.setAppSid(payment.getAppSid());
  605. record.setCustomerName(payment.getCustomerName());
  606. record.setDeviceId(payment.getDeviceId());
  607. record.setConv(payment.getConv());
  608. record.setPayment(payment.getPayment());
  609. record.setGmv(payment.getGmv());
  610. record.setAct(payment.getAct());
  611. record.setTu(payment.getTu());
  612. record.setClkTime(payment.getClkTime());
  613. record.setCreatedAt(Instant.now());
  614. return record;
  615. }
  616. private static ConversionRecord toHonorConversionRecord(PaymentInfo payment, HonorBidRecord bid) {
  617. ConversionRecord record = new ConversionRecord();
  618. record.setDedupeKey(conversionDedupeKey(payment));
  619. record.setQk(payment.getQk());
  620. record.setMedia("honor");
  621. record.setTagId(bid != null ? bid.getTagId() : null);
  622. record.setDate(payment.getDate());
  623. record.setAppSid(payment.getAppSid());
  624. record.setCustomerName(payment.getCustomerName());
  625. record.setDeviceId(payment.getDeviceId());
  626. record.setConv(payment.getConv());
  627. record.setPayment(payment.getPayment());
  628. record.setGmv(payment.getGmv());
  629. record.setAct(payment.getAct());
  630. record.setTu(payment.getTu());
  631. record.setClkTime(payment.getClkTime());
  632. record.setCreatedAt(Instant.now());
  633. return record;
  634. }
  635. private static ConversionRecord toKuaishouConversionRecord(PaymentInfo payment, KuaishouBidRecord bid) {
  636. ConversionRecord record = new ConversionRecord();
  637. record.setDedupeKey(conversionDedupeKey(payment));
  638. record.setQk(payment.getQk());
  639. record.setMedia("kuaishou");
  640. record.setTagId(bid != null ? bid.getTagId() : null);
  641. record.setDate(payment.getDate());
  642. record.setAppSid(payment.getAppSid());
  643. record.setCustomerName(payment.getCustomerName());
  644. record.setDeviceId(payment.getDeviceId());
  645. record.setConv(payment.getConv());
  646. record.setPayment(payment.getPayment());
  647. record.setGmv(payment.getGmv());
  648. record.setAct(payment.getAct());
  649. record.setTu(payment.getTu());
  650. record.setClkTime(payment.getClkTime());
  651. record.setCreatedAt(Instant.now());
  652. return record;
  653. }
  654. static String conversionDedupeKey(PaymentInfo payment) {
  655. return String.format("%s:%s:%s:%d",
  656. payment.getQk(),
  657. payment.getDate(),
  658. payment.getDeviceId(),
  659. payment.getAct());
  660. }
  661. static String callbackDedupeKey(PaymentInfo payment, BidRecord bid, String actionType) {
  662. return String.format("%s:%s:%s:%s:%s:%d:%s",
  663. bid.getMedia(),
  664. payment.getQk(),
  665. payment.getDate(),
  666. payment.getDeviceId(),
  667. bid.getTagId(),
  668. payment.getAct(),
  669. actionType);
  670. }
  671. static String legacyCallbackDedupeKey(PaymentInfo payment, String media) {
  672. return String.format("%s:%s:%s:%s:%d:%d",
  673. media,
  674. payment.getQk(),
  675. payment.getDate(),
  676. payment.getDeviceId(),
  677. payment.getAct(),
  678. payment.getAct());
  679. }
  680. static String honorCallbackDedupeKey(PaymentInfo payment, HonorBidRecord bid, int conversionId) {
  681. return String.format("honor:%s:%s:%s:%s:%d:%d",
  682. payment.getQk(),
  683. payment.getDate(),
  684. payment.getDeviceId(),
  685. bid.getTagId(),
  686. payment.getAct(),
  687. conversionId);
  688. }
  689. static String kuaishouCallbackDedupeKey(PaymentInfo payment, KuaishouBidRecord bid, int eventType) {
  690. return String.format("kuaishou:%s:%s:%s:%s:%d:%d",
  691. payment.getQk(),
  692. payment.getDate(),
  693. payment.getDeviceId(),
  694. bid.getTagId(),
  695. payment.getAct(),
  696. eventType);
  697. }
  698. private DeductionHandling handleTencentDeduction(CallbackTask task) {
  699. if (!task.isTencent() || task.tencentBid == null) {
  700. return DeductionHandling.SEND;
  701. }
  702. int deductionRate = resolveDeductionRate(task.tencentBid);
  703. if (task.payment.getAct() == 2001 || deductionRate <= 0) {
  704. return DeductionHandling.SEND;
  705. }
  706. if (!isNewTrackingLink(task.tencentBid)) {
  707. return DeductionHandling.SEND;
  708. }
  709. if (!hotStore.markConversionSeen(task.callbackKey)) {
  710. log.info("[ConversionSync] 腾讯转化已见过,跳过后续发送 | callbackKey={} | qk={} | accountId={}",
  711. task.callbackKey, task.payment.getQk(), safe(task.tencentBid.getAccountId()));
  712. return DeductionHandling.SKIP_DUPLICATE;
  713. }
  714. RedisHotStore.DeductionDecision decision = hotStore.evaluateDeduction(
  715. task.tencentBid.getAccountId(),
  716. task.payment.getAct(),
  717. resolveTrackingVersion(task.tencentBid),
  718. task.payment.getDate(),
  719. deductionRate
  720. );
  721. boolean deduct = decision.isShouldDeduct();
  722. log.info("[ConversionSync] 腾讯转化扣量判定 | qk={} | act={} | accountId={} | version={} | rate={} | totalSeen={} | totalDeducted={} | callbackKey={} | deduct={}",
  723. task.payment.getQk(), task.payment.getAct(), safe(task.tencentBid.getAccountId()),
  724. resolveTrackingVersion(task.tencentBid), deductionRate,
  725. decision.getTotalSeen(), decision.getTotalDeducted(), task.callbackKey, deduct);
  726. return deduct ? DeductionHandling.DEDUCT : DeductionHandling.SEND;
  727. }
  728. private static boolean isNewTrackingLink(BidRecord bid) {
  729. if (bid == null || bid.getMediaParams() == null) {
  730. return false;
  731. }
  732. String flag = bid.getMediaParams().get(BidRecord.NEW_LINK_FLAG_KEY);
  733. if (flag != null && Boolean.parseBoolean(flag)) {
  734. return true;
  735. }
  736. String version = bid.getMediaParams().get(BidRecord.TRACKING_VERSION_KEY);
  737. return "v2".equalsIgnoreCase(version)
  738. || "v3".equalsIgnoreCase(version)
  739. || "v4".equalsIgnoreCase(version);
  740. }
  741. private MediaCallbackRecord buildDeductedCallbackRecord(CallbackTask task) {
  742. MediaCallbackRecord record = new MediaCallbackRecord();
  743. Instant now = Instant.now();
  744. record.setDedupeKey(task.callbackKey);
  745. record.setMedia("tencent");
  746. record.setQk(task.payment.getQk());
  747. String callbackUrl = task.tencentBid.getMediaParams() != null
  748. ? task.tencentBid.getMediaParams().get("callback")
  749. : null;
  750. record.setCallbackUrl(callbackUrl != null ? callbackUrl : "");
  751. record.setEventType(task.payment.getAct());
  752. record.setEventTimeMs(now.toEpochMilli());
  753. record.setPurchase(task.payment.getPayment());
  754. record.setStatus(0);
  755. record.setOk(false);
  756. record.setAttempt(1);
  757. record.setErrorMessage("deducted_by_new_link_rate");
  758. record.setDispatchStatus(MediaCallbackRecord.DISPATCH_STATUS_DEDUCTED);
  759. record.setTrackingVersion(resolveTrackingVersion(task.tencentBid));
  760. record.setCreatedAt(now);
  761. int deductionRate = resolveDeductionRate(task.tencentBid);
  762. log.info("[ConversionSync] 腾讯转化扣量 | qk={} | act={} | rate={} | tagId={} | accountId={} | callbackKey={}",
  763. task.payment.getQk(), task.payment.getAct(), deductionRate,
  764. task.tencentBid.getTagId(), safe(task.tencentBid.getAccountId()), task.callbackKey);
  765. return record;
  766. }
  767. private boolean handleKuaishouDeduction(CallbackTask task) {
  768. if (!task.isKuaishou() || task.kuaishouBid == null || kuaishouHotStore == null) {
  769. return false;
  770. }
  771. int deductionRate = resolveKuaishouDeductionRate(task.kuaishouBid);
  772. if (task.payment.getAct() == 2001 || deductionRate <= 0) {
  773. return false;
  774. }
  775. if (!kuaishouHotStore.markConversionSeen(task.callbackKey)) {
  776. log.info("[ConversionSync] 快手转化已见过,跳过重复扣量判断 | callbackKey={} | qk={} | accountId={}",
  777. task.callbackKey, task.payment.getQk(), safe(task.kuaishouBid.getAccountId()));
  778. return false;
  779. }
  780. KuaishouHotStore.DeductionDecision decision = kuaishouHotStore.evaluateDeduction(
  781. task.kuaishouBid.getAccountId(),
  782. task.payment.getAct(),
  783. task.payment.getDate(),
  784. deductionRate
  785. );
  786. boolean deduct = decision.isShouldDeduct();
  787. log.info("[ConversionSync] 快手转化扣量判定 | qk={} | act={} | accountId={} | tagId={} | rate={} | totalSeen={} | totalDeducted={} | callbackKey={} | deduct={}",
  788. task.payment.getQk(), task.payment.getAct(), safe(task.kuaishouBid.getAccountId()),
  789. safe(task.kuaishouBid.getTagId()), deductionRate,
  790. decision.getTotalSeen(), decision.getTotalDeducted(), task.callbackKey, deduct);
  791. return deduct;
  792. }
  793. private KuaishouMediaCallbackRecord buildDeductedKuaishouCallbackRecord(CallbackTask task, int deductionRate) {
  794. KuaishouMediaCallbackRecord record = new KuaishouMediaCallbackRecord();
  795. Instant now = Instant.now();
  796. record.setDedupeKey(task.callbackKey);
  797. record.setQk(task.payment.getQk());
  798. record.setTagId(task.kuaishouBid.getTagId());
  799. String callbackUrl = task.kuaishouBid.getMediaParams() != null
  800. ? task.kuaishouBid.getMediaParams().get(KuaishouBidRecord.CALLBACK_KEY)
  801. : null;
  802. record.setCallbackUrl(callbackUrl != null ? callbackUrl : "");
  803. record.setEventType(task.kuaishouTagEvent.getEventType());
  804. record.setEventTimeMs(now.toEpochMilli());
  805. record.setPurchase(Math.max(task.payment.getPayment(), 0D));
  806. record.setDeductionRate(deductionRate);
  807. record.setDirectMatch(false);
  808. record.setDispatchStatus(KuaishouMediaCallbackRecord.DISPATCH_STATUS_DEDUCTED);
  809. record.setStatus(0);
  810. record.setOk(false);
  811. record.setAttempt(1);
  812. record.setErrorMessage("deducted_by_rate");
  813. record.setCreatedAt(now);
  814. log.info("[ConversionSync] 快手转化扣量 | qk={} | act={} | rate={} | tagId={} | accountId={} | callbackKey={}",
  815. task.payment.getQk(), task.payment.getAct(), deductionRate,
  816. task.kuaishouBid.getTagId(), safe(task.kuaishouBid.getAccountId()), task.callbackKey);
  817. return record;
  818. }
  819. private static String resolveTrackingVersion(BidRecord bid) {
  820. if (bid == null || bid.getMediaParams() == null) {
  821. return "v1";
  822. }
  823. String version = bid.getMediaParams().get(BidRecord.TRACKING_VERSION_KEY);
  824. return (version == null || version.isBlank()) ? "v1" : version;
  825. }
  826. private int resolveDeductionRate(BidRecord bid) {
  827. if (bid == null || bid.getMediaParams() == null) {
  828. return 0;
  829. }
  830. String rawRate = bid.getMediaParams().get(BidRecord.DEDUCTION_RATE_KEY);
  831. if (rawRate != null && !rawRate.isBlank()) {
  832. try {
  833. return sanitizeDeductionRate(Integer.parseInt(rawRate.trim()));
  834. } catch (NumberFormatException ignored) {
  835. }
  836. }
  837. return 0;
  838. }
  839. private int resolveKuaishouDeductionRate(KuaishouBidRecord bid) {
  840. if (bid == null || bid.getMediaParams() == null) {
  841. return 0;
  842. }
  843. String rawRate = bid.getMediaParams().get(KuaishouBidRecord.DEDUCTION_RATE_KEY);
  844. if (rawRate != null && !rawRate.isBlank()) {
  845. try {
  846. return sanitizeDeductionRate(Integer.parseInt(rawRate.trim()));
  847. } catch (NumberFormatException ignored) {
  848. }
  849. }
  850. return 0;
  851. }
  852. private static String safe(String value) {
  853. return value == null ? "" : value;
  854. }
  855. private static int sanitizeDeductionRate(int rate) {
  856. if (rate < 0) {
  857. return 0;
  858. }
  859. return Math.min(rate, 100);
  860. }
  861. private static HonorMediaCallbackRecord toHonorCallbackRecord(CallbackTask task, MediaCallbackRecord record) {
  862. HonorMediaCallbackRecord callback = new HonorMediaCallbackRecord();
  863. callback.setDedupeKey(record.getDedupeKey());
  864. callback.setQk(record.getQk());
  865. callback.setTagId(task.tagId);
  866. callback.setCallbackUrl(record.getCallbackUrl());
  867. callback.setEventType(record.getEventType());
  868. callback.setEventTimeMs(record.getEventTimeMs());
  869. callback.setPurchase(record.getPurchase());
  870. callback.setStatus(record.getStatus());
  871. callback.setOk(record.isOk());
  872. callback.setAttempt(record.getAttempt());
  873. callback.setResponseBody(record.getResponseBody());
  874. callback.setErrorMessage(record.getErrorMessage());
  875. callback.setRequestBody(record.getRequestBody());
  876. callback.setCreatedAt(record.getCreatedAt());
  877. return callback;
  878. }
  879. private static KuaishouMediaCallbackRecord toKuaishouCallbackRecord(CallbackTask task, MediaCallbackRecord record) {
  880. KuaishouMediaCallbackRecord callback = new KuaishouMediaCallbackRecord();
  881. callback.setDedupeKey(record.getDedupeKey());
  882. callback.setQk(record.getQk());
  883. callback.setTagId(task.tagId);
  884. callback.setCallbackUrl(record.getCallbackUrl());
  885. callback.setEventType(record.getEventType());
  886. callback.setEventTimeMs(record.getEventTimeMs());
  887. callback.setPurchase(record.getPurchase());
  888. callback.setDeductionRate(parseKuaishouDeductionRate(task.kuaishouBid));
  889. callback.setStatus(record.getStatus());
  890. callback.setOk(record.isOk());
  891. callback.setAttempt(record.getAttempt());
  892. callback.setResponseBody(record.getResponseBody());
  893. callback.setErrorMessage(record.getErrorMessage());
  894. callback.setRequestBody(record.getRequestBody());
  895. callback.setDispatchStatus(record.getDispatchStatus());
  896. callback.setDirectMatch(!KuaishouMediaCallbackRecord.DISPATCH_STATUS_DEDUCTED.equals(record.getDispatchStatus()));
  897. callback.setCreatedAt(record.getCreatedAt());
  898. return callback;
  899. }
  900. private static MediaCallbackRecord toCallbackRecord(CallbackTask task, HonorMediaCallbackRecord honorRecord) {
  901. MediaCallbackRecord callbackRecord = new MediaCallbackRecord();
  902. callbackRecord.setDedupeKey(task.callbackKey);
  903. callbackRecord.setMedia("honor");
  904. callbackRecord.setQk(honorRecord.getQk());
  905. callbackRecord.setCallbackUrl(honorRecord.getCallbackUrl());
  906. callbackRecord.setEventType(honorRecord.getEventType());
  907. callbackRecord.setEventTimeMs(honorRecord.getEventTimeMs());
  908. callbackRecord.setPurchase(honorRecord.getPurchase());
  909. callbackRecord.setStatus(honorRecord.getStatus());
  910. callbackRecord.setOk(honorRecord.isOk());
  911. callbackRecord.setAttempt(honorRecord.getAttempt());
  912. callbackRecord.setResponseBody(honorRecord.getResponseBody());
  913. callbackRecord.setErrorMessage(honorRecord.getErrorMessage());
  914. callbackRecord.setRequestBody(honorRecord.getRequestBody());
  915. callbackRecord.setTrackingVersion("v1");
  916. callbackRecord.setCreatedAt(honorRecord.getCreatedAt());
  917. return callbackRecord;
  918. }
  919. private static MediaCallbackRecord toCallbackRecord(CallbackTask task, KuaishouMediaCallbackRecord kuaishouRecord) {
  920. MediaCallbackRecord callbackRecord = new MediaCallbackRecord();
  921. callbackRecord.setDedupeKey(task.callbackKey);
  922. callbackRecord.setMedia("kuaishou");
  923. callbackRecord.setQk(kuaishouRecord.getQk());
  924. callbackRecord.setCallbackUrl(kuaishouRecord.getCallbackUrl());
  925. callbackRecord.setEventType(kuaishouRecord.getEventType());
  926. callbackRecord.setEventTimeMs(kuaishouRecord.getEventTimeMs());
  927. callbackRecord.setPurchase(kuaishouRecord.getPurchase());
  928. callbackRecord.setStatus(kuaishouRecord.getStatus());
  929. callbackRecord.setOk(kuaishouRecord.isOk());
  930. callbackRecord.setAttempt(kuaishouRecord.getAttempt());
  931. callbackRecord.setResponseBody(kuaishouRecord.getResponseBody());
  932. callbackRecord.setErrorMessage(kuaishouRecord.getErrorMessage());
  933. callbackRecord.setRequestBody(kuaishouRecord.getRequestBody());
  934. callbackRecord.setDispatchStatus(kuaishouRecord.getDispatchStatus());
  935. callbackRecord.setTrackingVersion(kuaishouRecord.isDirectMatch() ? "direct" : "deducted");
  936. callbackRecord.setCreatedAt(kuaishouRecord.getCreatedAt());
  937. return callbackRecord;
  938. }
  939. private static int parseKuaishouDeductionRate(KuaishouBidRecord bid) {
  940. if (bid == null || bid.getMediaParams() == null) {
  941. return 0;
  942. }
  943. String rawRate = bid.getMediaParams().get(KuaishouBidRecord.DEDUCTION_RATE_KEY);
  944. if (rawRate == null || rawRate.isBlank()) {
  945. return 0;
  946. }
  947. try {
  948. int rate = Integer.parseInt(rawRate.trim());
  949. if (rate < 0) {
  950. return 0;
  951. }
  952. return Math.min(rate, 100);
  953. } catch (NumberFormatException ignored) {
  954. return 0;
  955. }
  956. }
  957. // ─── 内部数据结构 ────────────────────────────────────────────────────────
  958. private static class BidLookup {
  959. private final Map<String, BidRecord> tencentBids;
  960. private final Map<String, HonorBidRecord> honorBids;
  961. private final Map<String, KuaishouBidRecord> kuaishouBids;
  962. private BidLookup(Map<String, BidRecord> tencentBids,
  963. Map<String, HonorBidRecord> honorBids,
  964. Map<String, KuaishouBidRecord> kuaishouBids) {
  965. this.tencentBids = tencentBids;
  966. this.honorBids = honorBids;
  967. this.kuaishouBids = kuaishouBids;
  968. }
  969. }
  970. private static class CallbackTask {
  971. private final String media;
  972. private final PaymentInfo payment;
  973. private final String tagId;
  974. private final String callbackKey;
  975. private final BidRecord tencentBid;
  976. private final String tencentActionType;
  977. private final HonorBidRecord honorBid;
  978. private final Integer honorConversionId;
  979. private final KuaishouBidRecord kuaishouBid;
  980. private final KuaishouTagEventRecord kuaishouTagEvent;
  981. private CallbackTask(String media,
  982. PaymentInfo payment,
  983. String tagId,
  984. String callbackKey,
  985. BidRecord tencentBid,
  986. String tencentActionType,
  987. HonorBidRecord honorBid,
  988. Integer honorConversionId,
  989. KuaishouBidRecord kuaishouBid,
  990. KuaishouTagEventRecord kuaishouTagEvent) {
  991. this.media = media;
  992. this.payment = payment;
  993. this.tagId = tagId;
  994. this.callbackKey = callbackKey;
  995. this.tencentBid = tencentBid;
  996. this.tencentActionType = tencentActionType;
  997. this.honorBid = honorBid;
  998. this.honorConversionId = honorConversionId;
  999. this.kuaishouBid = kuaishouBid;
  1000. this.kuaishouTagEvent = kuaishouTagEvent;
  1001. }
  1002. private static CallbackTask tencent(PaymentInfo payment,
  1003. BidRecord bid,
  1004. String callbackKey,
  1005. String actionType) {
  1006. return new CallbackTask("tencent", payment, bid.getTagId(), callbackKey, bid, actionType, null, null, null, null);
  1007. }
  1008. private static CallbackTask honor(PaymentInfo payment,
  1009. HonorBidRecord bid,
  1010. String callbackKey,
  1011. Integer conversionId) {
  1012. return new CallbackTask("honor", payment, bid.getTagId(), callbackKey, null, null, bid, conversionId, null, null);
  1013. }
  1014. private static CallbackTask kuaishou(PaymentInfo payment,
  1015. KuaishouBidRecord bid,
  1016. String callbackKey,
  1017. KuaishouTagEventRecord tagEvent) {
  1018. return new CallbackTask("kuaishou", payment, bid.getTagId(), callbackKey, null, null, null, null, bid, tagEvent);
  1019. }
  1020. private boolean isTencent() {
  1021. return "tencent".equals(media);
  1022. }
  1023. private boolean isHonor() {
  1024. return "honor".equals(media);
  1025. }
  1026. private boolean isKuaishou() {
  1027. return "kuaishou".equals(media);
  1028. }
  1029. }
  1030. private static class CallbackDispatchResult {
  1031. private final String media;
  1032. private final CallbackTask task;
  1033. private final MediaCallbackRecord record;
  1034. private CallbackDispatchResult(String media, CallbackTask task, MediaCallbackRecord record) {
  1035. this.media = media;
  1036. this.task = task;
  1037. this.record = record;
  1038. }
  1039. private static CallbackDispatchResult tencent(CallbackTask task, MediaCallbackRecord record) {
  1040. return new CallbackDispatchResult("tencent", task, record);
  1041. }
  1042. private static CallbackDispatchResult honor(CallbackTask task, MediaCallbackRecord record) {
  1043. return new CallbackDispatchResult("honor", task, record);
  1044. }
  1045. private static CallbackDispatchResult kuaishou(CallbackTask task, MediaCallbackRecord record) {
  1046. return new CallbackDispatchResult("kuaishou", task, record);
  1047. }
  1048. private boolean isHonor() {
  1049. return "honor".equals(media);
  1050. }
  1051. private boolean isKuaishou() {
  1052. return "kuaishou".equals(media);
  1053. }
  1054. }
  1055. private enum DeductionHandling {
  1056. SEND,
  1057. DEDUCT,
  1058. SKIP_DUPLICATE
  1059. }
  1060. public static class SyncResult {
  1061. public final AtomicInteger fetched = new AtomicInteger();
  1062. public final AtomicInteger matched = new AtomicInteger();
  1063. public final AtomicInteger sent = new AtomicInteger();
  1064. public final AtomicInteger skipped = new AtomicInteger();
  1065. public final AtomicInteger failed = new AtomicInteger();
  1066. public final AtomicInteger alreadySent = new AtomicInteger();
  1067. }
  1068. }