yumeng пре 3 недеља
родитељ
комит
75e2ceb750

+ 12 - 3
src/main/java/com/adx/tencent/AppConfiguration.java

@@ -162,11 +162,20 @@ public class AppConfiguration {
     // ─── Tencent 客户端 + Sync 服务 ──────────────────────────────────────────
 
     @Bean
-    public TencentClient tencentClient(ObjectMapper objectMapper) {
+    public TencentClient tencentClient(ObjectMapper objectMapper, StringRedisTemplate redisTemplate) {
+        // token 从 Redis 动态获取,支持外部刷新
+        String tokenRedisKey = props.getTencentAccessTokenRedisKey();
+        java.util.function.Supplier<String> tokenSupplier;
+        if (tokenRedisKey != null && !tokenRedisKey.isBlank()) {
+            tokenSupplier = () -> redisTemplate.opsForValue().get(tokenRedisKey);
+        } else {
+            // 兜底:使用配置文件中的静态 token
+            String staticToken = props.getTencentAccessToken();
+            tokenSupplier = () -> staticToken;
+        }
         return new TencentClient(
                 props.getTencentApiBaseUrl(),
-                props.getTencentAccessToken(),
-                props.getTencentAccountId(),
+                tokenSupplier,
                 props.getTencentAndroidUserActionSetId(),
                 props.getTencentIosUserActionSetId(),
                 props.getTencentActMap(),

+ 3 - 3
src/main/java/com/adx/tencent/config/AppProperties.java

@@ -41,7 +41,7 @@ public class AppProperties {
     // Tencent API
     private String tencentApiBaseUrl = "https://api.e.qq.com/v3.0";
     private String tencentAccessToken;
-    private long tencentAccountId;
+    private String tencentAccessTokenRedisKey;
     private long tencentAndroidUserActionSetId;
     private long tencentIosUserActionSetId;
 
@@ -152,8 +152,8 @@ public class AppProperties {
     public String getTencentAccessToken() { return tencentAccessToken; }
     public void setTencentAccessToken(String v) { this.tencentAccessToken = v; }
 
-    public long getTencentAccountId() { return tencentAccountId; }
-    public void setTencentAccountId(long v) { this.tencentAccountId = v; }
+    public String getTencentAccessTokenRedisKey() { return tencentAccessTokenRedisKey; }
+    public void setTencentAccessTokenRedisKey(String v) { this.tencentAccessTokenRedisKey = v; }
 
     public long getTencentAndroidUserActionSetId() { return tencentAndroidUserActionSetId; }
     public void setTencentAndroidUserActionSetId(long v) { this.tencentAndroidUserActionSetId = v; }

+ 12 - 3
src/main/java/com/adx/tencent/httpapi/TrackingController.java

@@ -83,7 +83,7 @@ public class TrackingController {
             throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "tencent placement not configured");
         }
 
-        log.info("[impression] traceId={}, ip={}, params={}", traceId, clientIp(request), params);
+        log.info("[曝光] 收到曝光请求 | traceId={} | IP={} | 全部入参: {}", traceId, clientIp(request), params);
 
         Map<String, Object> bidRequest = mediaBidRequest(request, "tencent", traceId, params, tencentPlacement);
         NormalizedBidResponse response = adxClient.requestBid(bidRequest);
@@ -143,7 +143,7 @@ public class TrackingController {
             throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "missing media trace id (click_id/oaid/imei/idfa)");
         }
 
-        log.info("[click] traceId={}, ip={}, params={}", traceId, clientIp(request), params);
+        log.info("[点击] 收到点击请求 | traceId={} | IP={} | 全部入参: {}", traceId, clientIp(request), params);
 
         // 先从 Redis 按 mediaTrace 查找已有 bid
         BidRecord record = hotStore.findBidByMediaTrace("tencent", traceId);
@@ -170,9 +170,16 @@ public class TrackingController {
         log.info("[click] 点击上报完成, qk={}, results={}", record.getQk(), trackingResults);
         recordTrackingResults(record.getQk(), "click", trackingResults);
 
-        // 重定向到 landing page 或 app store link
+        // 点击处理完成,缩减 Redis 数据(清除 clickUrls/landingPage/appStoreLink)
         String destination = record.getAppStoreLink() != null && !record.getAppStoreLink().isBlank()
                 ? record.getAppStoreLink() : record.getLandingPage();
+        try {
+            hotStore.shrinkAfterClick(record);
+        } catch (Exception e) {
+            log.warn("[click] Redis缩减失败(不影响主流程), qk={}, error={}", record.getQk(), e.getMessage());
+        }
+
+        // 重定向到 landing page 或 app store link
         if (destination != null && !destination.isBlank()) {
             log.info("[click] 302重定向, qk={}, destination={}", record.getQk(), destination);
             response.sendRedirect(destination);
@@ -194,6 +201,8 @@ public class TrackingController {
         r.setMedia(media);
         r.setMediaTraceId(traceId);
         r.setPlatform(mediaParams.get("baidu_platform"));
+        r.setAdId(mediaParams.get("ad_id"));
+        r.setAccountId(mediaParams.get("account_id"));
         r.setMediaParams(mediaParams);
         r.setCreatedAt(Instant.now());
         if (response.getBid() != null) {

+ 39 - 0
src/main/java/com/adx/tencent/storage/RedisHotStore.java

@@ -110,6 +110,45 @@ public class RedisHotStore {
 
     // ─── BidFinder ───────────────────────────────────────────────────────────
 
+    /**
+     * 点击处理完成后,将 Redis 中的 bid 记录缩减为仅保留转化匹配所需字段。
+     * 清除 clickUrls、landingPage、appStoreLink 以释放内存。
+     */
+    public void shrinkAfterClick(BidRecord record) {
+        try {
+            String slimPayload = objectMapper.writeValueAsString(record.toPostClickRecord());
+            byte[] slimBytes = slimPayload.getBytes(StandardCharsets.UTF_8);
+            // 获取剩余 TTL,保持原有过期时间
+            Long remainingTtl = redis.getExpire(bidKey(record.getQk()), java.util.concurrent.TimeUnit.MILLISECONDS);
+            if (remainingTtl == null || remainingTtl <= 0) remainingTtl = bidTtl.toMillis();
+            long ttlMs = remainingTtl;
+
+            redis.executePipelined((org.springframework.data.redis.connection.RedisConnection conn) -> {
+                if (record.getQk() != null && !record.getQk().isEmpty()) {
+                    conn.stringCommands().set(
+                            bidKey(record.getQk()).getBytes(StandardCharsets.UTF_8),
+                            slimBytes,
+                            org.springframework.data.redis.core.types.Expiration.milliseconds(ttlMs),
+                            org.springframework.data.redis.connection.RedisStringCommands.SetOption.UPSERT
+                    );
+                }
+                if (record.getMedia() != null && !record.getMedia().isEmpty()
+                        && record.getMediaTraceId() != null && !record.getMediaTraceId().isEmpty()) {
+                    conn.stringCommands().set(
+                            mediaTraceKey(record.getMedia(), record.getMediaTraceId()).getBytes(StandardCharsets.UTF_8),
+                            slimBytes,
+                            org.springframework.data.redis.core.types.Expiration.milliseconds(ttlMs),
+                            org.springframework.data.redis.connection.RedisStringCommands.SetOption.UPSERT
+                    );
+                }
+                return null;
+            });
+        } catch (Exception e) {
+            // 缩减失败不影响主流程,仅记录日志
+            throw new RuntimeException("shrinkAfterClick failed", e);
+        }
+    }
+
     public BidRecord findBidByQk(String qk) {
         String payload = redis.opsForValue().get(bidKey(qk));
         if (payload == null) return null;

+ 1 - 1
src/main/java/com/adx/tencent/storage/TiDBColdStore.java

@@ -153,7 +153,7 @@ public class TiDBColdStore {
             BidEventMapper mapper = session.getMapper(BidEventMapper.class);
             mapper.insertOrUpdate(
                 record.getQk(), record.getMedia(), record.getMediaTraceId(),
-                record.getPlatform(),
+                record.getPlatform(), record.getAdId(), record.getAccountId(),
                 record.getReqId(), record.getBidId(), record.getCreativeId(),
                 record.getImpId(), record.getTagId(), record.getPrice() != null ? record.getPrice() : 0L,
                 showUrls, clickUrls, record.getLandingPage(),

+ 2 - 0
src/main/java/com/adx/tencent/storage/mapper/BidEventMapper.java

@@ -9,6 +9,8 @@ public interface BidEventMapper {
                         @Param("media") String media,
                         @Param("mediaTraceId") String mediaTraceId,
                         @Param("platform") String platform,
+                        @Param("adId") String adId,
+                        @Param("accountId") String accountId,
                         @Param("reqId") String reqId,
                         @Param("bidId") String bidId,
                         @Param("creativeId") String creativeId,

+ 20 - 0
src/main/java/com/adx/tencent/storage/model/BidRecord.java

@@ -13,6 +13,8 @@ public class BidRecord {
     @JsonProperty("media")        private String media;
     @JsonProperty("mediaTraceId") private String mediaTraceId;
     @JsonProperty("platform")     private String platform;
+    @JsonProperty("adId")         private String adId;
+    @JsonProperty("accountId")    private String accountId;
     @JsonProperty("reqId")        private String reqId;
     @JsonProperty("bidId")        private String bidId;
     @JsonProperty("creativeId")   private String creativeId;
@@ -35,6 +37,10 @@ public class BidRecord {
     public void setMediaTraceId(String v) { this.mediaTraceId = v; }
     public String getPlatform() { return platform; }
     public void setPlatform(String v) { this.platform = v; }
+    public String getAdId() { return adId; }
+    public void setAdId(String v) { this.adId = v; }
+    public String getAccountId() { return accountId; }
+    public void setAccountId(String v) { this.accountId = v; }
     public String getReqId() { return reqId; }
     public void setReqId(String v) { this.reqId = v; }
     public String getBidId() { return bidId; }
@@ -63,6 +69,20 @@ public class BidRecord {
     public void setCreatedAt(Instant v) { this.createdAt = v; }
 
     /**
+     * 返回点击处理完成后的最小化副本,仅保留转化匹配所需字段。
+     * 清除 clickUrls、landingPage、appStoreLink 以释放 Redis 内存。
+     */
+    public BidRecord toPostClickRecord() {
+        BidRecord slim = new BidRecord();
+        slim.qk = this.qk;
+        slim.media = this.media;
+        slim.mediaTraceId = this.mediaTraceId;
+        slim.platform = this.platform;
+        slim.mediaParams = this.mediaParams; // 已经是瘦身后的设备标识
+        return slim;
+    }
+
+    /**
      * 返回一个只包含 Redis 热存储所需关键字段的瘦身副本。
      * - 排除 reqId、bidId、creativeId、impId、tagId、price、showUrls、packageName
      * - mediaParams 只保留设备标识相关字段(转化回传需要)

+ 39 - 13
src/main/java/com/adx/tencent/tencent/TencentClient.java

@@ -19,6 +19,7 @@ import java.security.MessageDigest;
 import java.time.Duration;
 import java.time.Instant;
 import java.util.*;
+import java.util.function.Supplier;
 
 /**
  * 腾讯行为回传客户端。
@@ -31,21 +32,19 @@ public class TencentClient {
     private static final Logger log = LoggerFactory.getLogger(TencentClient.class);
 
     private final String apiBaseUrl;
-    private final String accessToken;
-    private final long accountId;
+    private final Supplier<String> accessTokenSupplier;
     private final long androidUserActionSetId;
     private final long iosUserActionSetId;
     private final Map<Integer, String> actMap;  // baiduAct -> tencentActionType
     private final HttpClient httpClient;
     private final ObjectMapper objectMapper;
 
-    public TencentClient(String apiBaseUrl, String accessToken, long accountId,
+    public TencentClient(String apiBaseUrl, Supplier<String> accessTokenSupplier,
                          long androidUserActionSetId, long iosUserActionSetId,
                          Map<Integer, String> actMapOverride,
                          ObjectMapper objectMapper) {
         this.apiBaseUrl = apiBaseUrl.replaceAll("/+$", "");
-        this.accessToken = accessToken;
-        this.accountId = accountId;
+        this.accessTokenSupplier = accessTokenSupplier;
         this.androidUserActionSetId = androidUserActionSetId;
         this.iosUserActionSetId = iosUserActionSetId;
         this.objectMapper = objectMapper;
@@ -105,23 +104,45 @@ public class TencentClient {
         // 构造请求 URL
         long timestamp = Instant.now().getEpochSecond();
         String nonce = UUID.randomUUID().toString().replace("-", "").substring(0, 32);
+        String currentToken = accessTokenSupplier.get();
+        if (currentToken == null || currentToken.isBlank()) {
+            record.setOk(false);
+            record.setErrorMessage("access_token is empty (check Redis key)");
+            log.warn("[Tencent] access_token 为空, qk={}", payment.getQk());
+            return record;
+        }
         String url = String.format("%s/user_actions/add?access_token=%s&timestamp=%d&nonce=%s",
-                apiBaseUrl, accessToken, timestamp, nonce);
+                apiBaseUrl, currentToken, timestamp, nonce);
         record.setCallbackUrl(url);
 
         // 构造请求 Body
         UserAction action = buildUserAction(bid, payment, actionType, eventTimeMs);
 
         long actionSetId = getUserActionSetId(platform);
+        // account_id: 从监测链接传入
+        long finalAccountId = 0;
+        if (bid.getAccountId() != null && !bid.getAccountId().isBlank()) {
+            try {
+                finalAccountId = Long.parseLong(bid.getAccountId());
+            } catch (NumberFormatException ignored) {}
+        }
+        if (finalAccountId == 0) {
+            log.warn("[Tencent] account_id 缺失, qk={}, 监测链接未传入account_id", payment.getQk());
+        }
         Map<String, Object> body = new LinkedHashMap<>();
-        body.put("account_id", accountId);
+        body.put("account_id", finalAccountId);
         body.put("user_action_set_id", actionSetId);
         body.put("actions", List.of(action));
+        // ad_id: 传入广告ID用于归因匹配
+        if (bid.getAdId() != null && !bid.getAdId().isBlank()) {
+            body.put("ad_id", bid.getAdId());
+        }
 
         try {
             byte[] payload = objectMapper.writeValueAsBytes(body);
-            log.info("[Tencent] sending user_action: qk={}, actionType={}, baiduAct={}",
-                    payment.getQk(), actionType, payment.getAct());
+            String payloadStr = new String(payload, java.nio.charset.StandardCharsets.UTF_8);
+            log.info("[Tencent] sending user_action: qk={}, actionType={}, baiduAct={}, url={}, body={}",
+                    payment.getQk(), actionType, payment.getAct(), url, payloadStr);
 
             HttpRequest request = HttpRequest.newBuilder()
                     .uri(URI.create(url))
@@ -149,8 +170,8 @@ public class TencentClient {
                 record.setErrorMessage("HTTP status: " + status);
             }
 
-            log.info("[Tencent] callback result: qk={}, actionType={}, status={}, ok={}",
-                    payment.getQk(), actionType, status, record.isOk());
+            log.info("[Tencent] callback result: qk={}, actionType={}, status={}, ok={}, response={}",
+                    payment.getQk(), actionType, status, record.isOk(), responseBody);
         } catch (Exception e) {
             record.setOk(false);
             record.setErrorMessage(e.getMessage());
@@ -176,8 +197,14 @@ public class TencentClient {
             // 重新构造请求(使用新的 timestamp 和 nonce)
             long timestamp = Instant.now().getEpochSecond();
             String nonce = UUID.randomUUID().toString().replace("-", "").substring(0, 32);
+            String currentToken = accessTokenSupplier.get();
+            if (currentToken == null || currentToken.isBlank()) {
+                retry.setOk(false);
+                retry.setErrorMessage("access_token is empty (check Redis key)");
+                return retry;
+            }
             String url = String.format("%s/user_actions/add?access_token=%s&timestamp=%d&nonce=%s",
-                    apiBaseUrl, accessToken, timestamp, nonce);
+                    apiBaseUrl, currentToken, timestamp, nonce);
 
             // 需要从 original 解析出原始 body 重发
             // 简化处理:直接用原始的 callbackUrl 重新发送一个空 body 触发
@@ -291,7 +318,6 @@ public class TencentClient {
         m.put(1, "ACTIVATE_APP");
         m.put(2, "PURCHASE");
         m.put(3, "REGISTER");
-        m.put(4, "ONE_DAY_RETENTION");
         m.put(5, "COMPLETE_ORDER");
         m.put(6, "START_APP");
         m.put(7, "CUSTOM");

+ 11 - 13
src/main/resources/application-dev.yml

@@ -15,20 +15,18 @@ adx:
 
   # --- Tencent API ---
   tencent-api-base-url: "https://api.e.qq.com/v3.0"
-  tencent-access-token: "50aab5d01ee39ed5fa6ce6054924d132"
-  tencent-account-id: 0
-  tencent-android-user-action-set-id: 0
-  tencent-ios-user-action-set-id: 0
+  tencent-access-token-redis-key: "adq:access_token:78578478"
+  tencent-android-user-action-set-id: 1222435591
+  tencent-ios-user-action-set-id: 1222435768
   tencent-act-map:
-    "1": ACTIVATE_APP
-    "2": PURCHASE
-    "3": REGISTER
-    "4": ONE_DAY_RETENTION
-    "5": COMPLETE_ORDER
-    "6": START_APP
-    "7": CUSTOM
-    "8": CUSTOM
-    "9": CUSTOM
+    "1": ACTIVATE_APP      # 激活
+    "2": PURCHASE          # 付费
+    "3": REGISTER          # 注册
+    "5": COMPLETE_ORDER    # 完成订单
+    "6": START_APP         # 启动应用
+    "7": CUSTOM            # 自定义-关键行为
+    "8": CUSTOM            # 自定义-7日留存
+    "9": CUSTOM            # 自定义-3日留存
 
   # --- Redis ---
   redis-addr: "cq-crs-47p6pmwo.sql.tencentcdb.com:28036"

+ 10 - 12
src/main/resources/application-prod.yml

@@ -4,7 +4,7 @@
 
 logging:
   file:
-    path: /var/log/adx-tencent
+    path: /data/webapp/tencent-baidu-tracking
 
 adx:
   # --- Baidu ADX ---
@@ -15,10 +15,8 @@ adx:
 
   # --- Tencent API ---
   tencent-api-base-url: "https://api.e.qq.com/v3.0"
-  tencent-access-token: "50aab5d01ee39ed5fa6ce6054924d132"
-  tencent-account-id: ${TENCENT_ACCOUNT_ID:0}
-  tencent-android-user-action-set-id: ${TENCENT_ANDROID_USER_ACTION_SET_ID:0}
-  tencent-ios-user-action-set-id: ${TENCENT_IOS_USER_ACTION_SET_ID:0}
+  tencent-android-user-action-set-id: 1222435591
+  tencent-ios-user-action-set-id: 1222435768
   tencent-act-map:
     "1": ACTIVATE_APP
     "2": PURCHASE
@@ -31,15 +29,15 @@ adx:
     "9": CUSTOM
 
   # --- Redis ---
-  redis-addr: "${REDIS_ADDR}"
-  redis-username: "${REDIS_USERNAME:}"
-  redis-password: "${REDIS_PASSWORD:}"
-  redis-db: 0
+  redis-addr: "172.30.0.25:6379"
+  redis-username: "hcst"
+  redis-password: "hcst@2026qwe.."
+  redis-db: 5
 
   # --- TiDB/MySQL ---
-  tidb-url: "${TIDB_URL}"
-  tidb-username: "${TIDB_USERNAME}"
-  tidb-password: "${TIDB_PASSWORD}"
+  tidb-url: "jdbc:mysql://139.186.172.149:3390/adx_tencent?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true"
+  tidb-username: "hcst"
+  tidb-password: "hcst@2025"
 
   # --- Task ---
   skip-leader-election: false

+ 0 - 1
src/main/resources/application-test.yml

@@ -16,7 +16,6 @@ adx:
   # --- Tencent API ---
   tencent-api-base-url: "https://api.e.qq.com/v3.0"
   tencent-access-token: "YOUR_TEST_ACCESS_TOKEN"
-  tencent-account-id: 0
   tencent-android-user-action-set-id: 0
   tencent-ios-user-action-set-id: 0
   tencent-act-map:

+ 6 - 2
src/main/resources/mapper/BidEventMapper.xml

@@ -4,11 +4,13 @@
 <mapper namespace="com.adx.tencent.storage.mapper.BidEventMapper">
     <insert id="insertOrUpdate">
         INSERT INTO tencent_ad_bid_events (
-            qk, media, media_trace_id, platform, req_id, bid_id, creative_id, imp_id, tag_id, price,
+            qk, media, media_trace_id, platform, ad_id, account_id,
+            req_id, bid_id, creative_id, imp_id, tag_id, price,
             show_urls, click_urls, landing_page, app_store_link, package_name,
             media_params, created_at
         ) VALUES (
-            #{qk}, #{media}, #{mediaTraceId}, #{platform}, #{reqId}, #{bidId}, #{creativeId},
+            #{qk}, #{media}, #{mediaTraceId}, #{platform}, #{adId}, #{accountId},
+            #{reqId}, #{bidId}, #{creativeId},
             #{impId}, #{tagId}, #{price}, #{showUrls}, #{clickUrls}, #{landingPage},
             #{appStoreLink}, #{packageName}, #{mediaParams}, #{createdAt}
         )
@@ -16,6 +18,8 @@
             media = VALUES(media),
             media_trace_id = VALUES(media_trace_id),
             platform = VALUES(platform),
+            ad_id = VALUES(ad_id),
+            account_id = VALUES(account_id),
             req_id = VALUES(req_id),
             bid_id = VALUES(bid_id),
             creative_id = VALUES(creative_id),

+ 6 - 0
src/main/resources/schema.sql

@@ -15,6 +15,8 @@ CREATE TABLE IF NOT EXISTS tencent_ad_bid_events (
     media VARCHAR(64) COMMENT '媒体来源(tencent)',
     media_trace_id VARCHAR(255) COMMENT '媒体追踪ID(click_id/oaid/imei/idfa)',
     platform VARCHAR(16) COMMENT '平台(android/ios)',
+    ad_id VARCHAR(128) COMMENT '腾讯广告ID',
+    account_id VARCHAR(64) COMMENT '腾讯广告主ID',
     req_id VARCHAR(512) COMMENT '百度ADX请求ID',
     bid_id VARCHAR(128) COMMENT '竞价ID',
     creative_id VARCHAR(128) COMMENT '创意ID',
@@ -103,3 +105,7 @@ CREATE TABLE IF NOT EXISTS tencent_media_callbacks (
 -- 如果旧表没有 platform 字段
 -- ALTER TABLE tencent_ad_bid_events ADD COLUMN platform VARCHAR(16) AFTER media_trace_id;
 -- ALTER TABLE tencent_ad_bid_events ADD KEY idx_tencent_ad_bid_events_platform_tag (platform, tag_id);
+
+-- 如果旧表没有 ad_id / account_id 字段
+-- ALTER TABLE tencent_ad_bid_events ADD COLUMN ad_id VARCHAR(128) COMMENT '腾讯广告ID' AFTER platform;
+-- ALTER TABLE tencent_ad_bid_events ADD COLUMN account_id VARCHAR(64) COMMENT '腾讯广告主ID' AFTER ad_id;