package com.adx.tencent.conversionsync; import com.adx.tencent.baidu.model.PaymentInfo; import com.adx.tencent.storage.RedisHotStore; import com.adx.tencent.storage.TiDBColdStore; import com.adx.tencent.storage.model.BidRecord; import com.adx.tencent.storage.model.MediaCallbackRecord; import com.adx.tencent.tencent.TencentClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.server.ResponseStatusException; import java.time.Instant; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; import static org.springframework.http.HttpStatus.BAD_REQUEST; import static org.springframework.http.HttpStatus.NOT_FOUND; public class ManualTencentCallbackService { private static final Logger log = LoggerFactory.getLogger(ManualTencentCallbackService.class); private final TiDBColdStore coldStore; private final RedisHotStore hotStore; private final TencentClient tencentClient; public ManualTencentCallbackService(TiDBColdStore coldStore, RedisHotStore hotStore, TencentClient tencentClient) { this.coldStore = coldStore; this.hotStore = hotStore; this.tencentClient = tencentClient; } public Map replayDeductedCallback(long id) { MediaCallbackRecord original = coldStore.getMediaCallbackById(id); if (original == null) { throw new ResponseStatusException(NOT_FOUND, "callback record not found"); } if (!"tencent".equals(original.getMedia())) { throw new ResponseStatusException(BAD_REQUEST, "callback record is not tencent"); } if (!MediaCallbackRecord.DISPATCH_STATUS_DEDUCTED.equals(original.getDispatchStatus())) { throw new ResponseStatusException(BAD_REQUEST, "callback record is not deducted"); } MediaCallbackRecord replay = replayOnce(original); replay.setId(original.getId()); coldStore.updateMediaCallback(replay); Map result = new LinkedHashMap<>(); result.put("id", id); result.put("dedupeKey", replay.getDedupeKey()); result.put("before", snapshot(original)); result.put("after", snapshot(replay)); return result; } private MediaCallbackRecord replayOnce(MediaCallbackRecord original) { if (original.getRequestBody() != null && !original.getRequestBody().isBlank()) { try { MediaCallbackRecord replay = tencentClient.retryCallback(original); replay.setDedupeKey(original.getDedupeKey()); replay.setAttempt(Math.max(original.getAttempt(), 1) + 1); if (replay.getTrackingVersion() == null || replay.getTrackingVersion().isBlank()) { replay.setTrackingVersion(original.getTrackingVersion()); } return replay; } catch (Exception e) { MediaCallbackRecord failed = copyForFailure(original); failed.setErrorMessage(e.getMessage()); return failed; } } BidRecord bid = hotStore.findBidByQk(original.getQk()); if (bid == null) { bid = coldStore.getBidByQk(original.getQk()); } if (bid == null) { throw new ResponseStatusException(NOT_FOUND, "bid not found for qk=" + original.getQk()); } DedupeInfo dedupeInfo = parseDedupeInfo(original); String actionType = dedupeInfo.actionType != null && !dedupeInfo.actionType.isBlank() ? dedupeInfo.actionType : tencentClient.conversionActionType(original.getEventType()); patchBidForReplay(bid, original, dedupeInfo); PaymentInfo payment = new PaymentInfo(); payment.setQk(original.getQk()); payment.setDate(dedupeInfo.date); payment.setDeviceId(dedupeInfo.deviceId); payment.setAct(original.getEventType()); payment.setPayment(original.getPurchase()); payment.setGmv(original.getPurchase()); MediaCallbackRecord replay = tencentClient.sendConversionEvent( bid, payment, resolvePlatform(bid), actionType ); replay.setDedupeKey(original.getDedupeKey()); replay.setAttempt(Math.max(original.getAttempt(), 1) + 1); if (replay.getTrackingVersion() == null || replay.getTrackingVersion().isBlank()) { replay.setTrackingVersion(original.getTrackingVersion()); } replay.setDispatchStatus(MediaCallbackRecord.DISPATCH_STATUS_SENT); replay.setCreatedAt(Instant.now()); log.info("[ManualTencentCallback] manual replay sent | qk={} | dedupeKey={} | ok={} | status={}", original.getQk(), original.getDedupeKey(), replay.isOk(), replay.getStatus()); return replay; } private void patchBidForReplay(BidRecord bid, MediaCallbackRecord original, DedupeInfo dedupeInfo) { if ((bid.getTagId() == null || bid.getTagId().isBlank()) && dedupeInfo.tagId != null && !dedupeInfo.tagId.isBlank()) { bid.setTagId(dedupeInfo.tagId); } Map params = bid.getMediaParams(); if (params == null) { params = new LinkedHashMap<>(); bid.setMediaParams(params); } else if (!(params instanceof LinkedHashMap)) { params = new LinkedHashMap<>(params); bid.setMediaParams(params); } if ((params.get("callback") == null || params.get("callback").isBlank()) && original.getCallbackUrl() != null && !original.getCallbackUrl().isBlank()) { params.put("callback", original.getCallbackUrl()); } } private static MediaCallbackRecord copyForFailure(MediaCallbackRecord original) { MediaCallbackRecord failed = new MediaCallbackRecord(); failed.setDedupeKey(original.getDedupeKey()); failed.setMedia(original.getMedia()); failed.setQk(original.getQk()); failed.setCallbackUrl(original.getCallbackUrl()); failed.setRequestBody(original.getRequestBody()); failed.setEventType(original.getEventType()); failed.setEventTimeMs(Instant.now().toEpochMilli()); failed.setPurchase(original.getPurchase()); failed.setStatus(original.getStatus()); failed.setOk(false); failed.setAttempt(Math.max(original.getAttempt(), 1) + 1); failed.setResponseBody(original.getResponseBody()); failed.setDispatchStatus(MediaCallbackRecord.DISPATCH_STATUS_SENT); failed.setTrackingVersion(original.getTrackingVersion()); failed.setCreatedAt(Instant.now()); return failed; } private static DedupeInfo parseDedupeInfo(MediaCallbackRecord record) { DedupeInfo info = new DedupeInfo(); String dedupeKey = record.getDedupeKey(); if (dedupeKey == null || dedupeKey.isBlank()) { return info; } String[] parts = dedupeKey.split(":"); if (parts.length < 7) { return info; } info.date = parts[2]; info.deviceId = parts[3]; info.tagId = parts[4]; info.actionType = joinTail(parts, 6); return info; } private static String joinTail(String[] parts, int start) { if (parts.length <= start) { return null; } StringBuilder builder = new StringBuilder(); for (int i = start; i < parts.length; i++) { if (i > start) { builder.append(':'); } builder.append(parts[i]); } return builder.toString(); } private static String resolvePlatform(BidRecord bid) { if (bid.getPlatform() != null && !bid.getPlatform().isBlank()) { return bid.getPlatform().toLowerCase(); } if (bid.getMediaParams() == null) { return "android"; } String platform = bid.getMediaParams().get("baidu_platform"); if (platform != null && !platform.isBlank()) { return platform.toLowerCase(); } String idfa = bid.getMediaParams().get("idfa"); if (idfa != null && !idfa.isBlank()) { return "ios"; } return "android"; } private static Map snapshot(MediaCallbackRecord record) { Map data = new LinkedHashMap<>(); data.put("id", record.getId()); data.put("dispatchStatus", record.getDispatchStatus()); data.put("attempt", record.getAttempt()); data.put("status", record.getStatus()); data.put("ok", record.isOk()); data.put("errorMessage", record.getErrorMessage()); data.put("callbackUrl", record.getCallbackUrl()); data.put("trackingVersion", record.getTrackingVersion()); return data; } private static class DedupeInfo { private String date; private String deviceId; private String tagId; private String actionType; } }