yumeng 2 هفته پیش
والد
کامیت
f778a18fa3

+ 27 - 15
src/main/java/com/adx/tencent/conversionsync/ConversionSyncService.java

@@ -103,6 +103,7 @@ public class ConversionSyncService {
             return;
         }
 
+
         // bid 不是腾讯,或没有设备标识
         if (!"tencent".equals(bid.getMedia())) {
             if (coldStore != null) {
@@ -119,13 +120,23 @@ public class ConversionSyncService {
             try { coldStore.saveConversion(conversionRecord(payment, bid.getMedia())); } catch (Exception ignored) {}
         }
 
+        String actionType = null;
+        if (tagEventResolver != null) {
+            actionType = tagEventResolver.resolveActionType(bid.getTagId(), payment.getAct());
+        }
+        if (actionType == null || actionType.isBlank()) {
+            actionType = tencentClient.conversionActionType(payment.getAct());
+            log.warn("[ConversionSync] 广告位回传方式未配置, 回退act-map | qk={} | tagId={} | act={} | actionType={}",
+                    payment.getQk(), bid.getTagId(), payment.getAct(), actionType);
+        }
+
         // 幂等检查
-        String callbackKey = callbackDedupeKey(payment, bid.getMedia());
+        String callbackKey = callbackDedupeKey(payment, bid, actionType);
         if (coldStore != null) {
             try {
-                String conversionKey = conversionDedupeKey(payment, bid.getMedia());
+                String legacyCallbackKey = legacyCallbackDedupeKey(payment, bid.getMedia());
                 boolean exists = coldStore.successfulCallbackExistsForEvent(
-                        callbackKey, conversionKey, payment.getAct());
+                        callbackKey, legacyCallbackKey, payment.getAct());
                 if (exists) {
                     result.alreadySent.incrementAndGet();
                     return;
@@ -135,18 +146,8 @@ public class ConversionSyncService {
             }
         }
 
-        // 发送到腾讯
         String platform = resolvePlatform(bid);
-        // 根据广告位(tag_id)解析回传方式作为 action_type:Redis 优先,DB 兜底
-        String actionTypeOverride = null;
-        if (tagEventResolver != null) {
-            actionTypeOverride = tagEventResolver.resolveEventType(bid.getTagId());
-        }
-        if (actionTypeOverride == null || actionTypeOverride.isBlank()) {
-            log.warn("[ConversionSync] 广告位回传方式未配置, 回退act-map | qk={} | tagId={} | act={}",
-                    payment.getQk(), bid.getTagId(), payment.getAct());
-        }
-        MediaCallbackRecord callbackRecord = tencentClient.sendConversionEvent(bid, payment, platform, actionTypeOverride);
+        MediaCallbackRecord callbackRecord = tencentClient.sendConversionEvent(bid, payment, platform, actionType);
         callbackRecord.setDedupeKey(callbackKey);
 
         if (callbackRecord.getStatus() == 0 && callbackRecord.getErrorMessage() != null) {
@@ -218,7 +219,18 @@ public class ConversionSyncService {
         return String.format("%s:%s:%s:%s:%d", media, p.getQk(), p.getDate(), p.getDeviceId(), p.getAct());
     }
 
-    static String callbackDedupeKey(PaymentInfo p, String media) {
+    static String callbackDedupeKey(PaymentInfo p, BidRecord bid, String actionType) {
+        return String.format("%s:%s:%s:%s:%s:%d:%s",
+                bid.getMedia(),
+                p.getQk(),
+                p.getDate(),
+                p.getDeviceId(),
+                bid.getTagId(),
+                p.getAct(),
+                actionType);
+    }
+
+    static String legacyCallbackDedupeKey(PaymentInfo p, String media) {
         return conversionDedupeKey(p, media) + ":" + p.getAct();
     }
 

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

@@ -117,8 +117,10 @@ public class TiDBColdStore {
             """,
             """
             CREATE TABLE IF NOT EXISTS tencent_tag_event (
-                tag_id VARCHAR(32) NOT NULL PRIMARY KEY,
-                event_type VARCHAR(128) NOT NULL COMMENT '回传方式'
+                tag_id VARCHAR(32) NOT NULL,
+                baidu_act INT NOT NULL COMMENT '百度转化行为',
+                tencent_action_type VARCHAR(128) NOT NULL COMMENT '腾讯回传行为',
+                PRIMARY KEY (tag_id, baidu_act)
             ) COMMENT '竞价事件记录'
             """
         };
@@ -141,6 +143,24 @@ public class TiDBColdStore {
             try {
                 stmt.execute("ALTER TABLE tencent_baidu_conversions ADD COLUMN gmv DOUBLE AFTER payment");
             } catch (Exception ignored) {}
+            try {
+                stmt.execute("ALTER TABLE tencent_tag_event ADD COLUMN baidu_act INT NULL COMMENT '百度转化行为' AFTER tag_id");
+            } catch (Exception ignored) {}
+            try {
+                stmt.execute("ALTER TABLE tencent_tag_event CHANGE COLUMN event_type tencent_action_type VARCHAR(128) NOT NULL COMMENT '腾讯回传行为'");
+            } catch (Exception ignored) {}
+            try {
+                stmt.execute("UPDATE tencent_tag_event SET baidu_act = 0 WHERE baidu_act IS NULL");
+            } catch (Exception ignored) {}
+            try {
+                stmt.execute("ALTER TABLE tencent_tag_event MODIFY COLUMN baidu_act INT NOT NULL");
+            } catch (Exception ignored) {}
+            try {
+                stmt.execute("ALTER TABLE tencent_tag_event DROP PRIMARY KEY");
+            } catch (Exception ignored) {}
+            try {
+                stmt.execute("ALTER TABLE tencent_tag_event ADD PRIMARY KEY (tag_id, baidu_act)");
+            } catch (Exception ignored) {}
             stmt.close();
         } catch (Exception e) {
             throw new RuntimeException("migrate failed", e);
@@ -263,11 +283,11 @@ public class TiDBColdStore {
     /**
      * 根据广告位ID查询回传方式配置(DB 兜底查询)。
      */
-    public TagEventRecord getTagEvent(String tagId) {
+    public TagEventRecord getTagEvent(String tagId, int baiduAct) {
         if (tagId == null || tagId.isBlank()) return null;
         try (SqlSession session = sqlSessionFactory.openSession(true)) {
             TagEventMapper mapper = session.getMapper(TagEventMapper.class);
-            return mapper.selectByTagId(tagId);
+            return mapper.selectByTagIdAndAct(tagId, baiduAct);
         }
     }
 

+ 3 - 2
src/main/java/com/adx/tencent/storage/mapper/TagEventMapper.java

@@ -15,7 +15,8 @@ public interface TagEventMapper {
     List<TagEventRecord> selectAll();
 
     /**
-     * 根据广告位ID查询回传方式配置。
+     * 根据广告位ID和百度行为查询回传方式配置。
      */
-    TagEventRecord selectByTagId(@Param("tagId") String tagId);
+    TagEventRecord selectByTagIdAndAct(@Param("tagId") String tagId,
+                                       @Param("baiduAct") int baiduAct);
 }

+ 10 - 5
src/main/java/com/adx/tencent/storage/model/TagEventRecord.java

@@ -2,17 +2,22 @@ package com.adx.tencent.storage.model;
 
 /**
  * 广告位回传方式记录,对应 tencent_tag_event 表。
- *   tag_id     广告位ID
- *   event_type 回传方式
+ *   tag_id              广告位ID
+ *   baidu_act           百度转化行为
+ *   tencent_action_type 腾讯回传行为
  */
 public class TagEventRecord {
 
     private String tagId;
-    private String eventType;
+    private int baiduAct;
+    private String tencentActionType;
 
     public String getTagId() { return tagId; }
     public void setTagId(String v) { this.tagId = v; }
 
-    public String getEventType() { return eventType; }
-    public void setEventType(String v) { this.eventType = v; }
+    public int getBaiduAct() { return baiduAct; }
+    public void setBaiduAct(int v) { this.baiduAct = v; }
+
+    public String getTencentActionType() { return tencentActionType; }
+    public void setTencentActionType(String v) { this.tencentActionType = v; }
 }

+ 26 - 12
src/main/java/com/adx/tencent/tagsync/TagEventResolver.java

@@ -8,8 +8,8 @@ import org.springframework.data.redis.core.StringRedisTemplate;
 
 /**
  * 广告位回传方式解析器。
- * 根据广告位ID(tag_id)解析回传方式(event_type)
- *   1. 优先从 Redis 读取(key = {prefix}{tagId}),命中直接返回;
+ * 根据广告位ID和百度行为解析腾讯回传方式
+ *   1. 优先从 Redis 读取(key = {prefix}{tagId}:{act}),命中直接返回;
  *   2. Redis 未命中时兜底查询 tencent_tag_event 表。
  * Redis key 前缀必须与 {@link TagEventSyncService} 保持一致。
  */
@@ -30,34 +30,48 @@ public class TagEventResolver {
     /**
      * 解析广告位回传方式。
      *
-     * @param tagId 广告位ID
+     * @param tagId    广告位ID
+     * @param baiduAct 百度转化行为
      * @return 回传方式;未配置时返回 null
      */
-    public String resolveEventType(String tagId) {
+    public String resolveActionType(String tagId, int baiduAct) {
         if (tagId == null || tagId.isBlank()) return null;
 
         // 1. Redis 优先
         try {
-            String cached = redis.opsForValue().get(keyPrefix + tagId);
+            String cached = redis.opsForValue().get(buildRedisKey(tagId, baiduAct));
+            if (cached != null && !cached.isBlank()) {
+                return cached;
+            }
+            cached = redis.opsForValue().get(buildRedisKey(tagId, 0));
             if (cached != null && !cached.isBlank()) {
                 return cached;
             }
         } catch (Exception e) {
-            log.warn("[TagEventResolver] 读取 Redis 失败, 回退DB查询 | tagId={} | error={}",
-                    tagId, e.getMessage());
+            log.warn("[TagEventResolver] 读取 Redis 失败, 回退DB查询 | tagId={} | act={} | error={}",
+                    tagId, baiduAct, e.getMessage());
         }
 
         // 2. DB 兜底
         try {
-            TagEventRecord record = coldStore.getTagEvent(tagId);
-            if (record != null && record.getEventType() != null && !record.getEventType().isBlank()) {
-                return record.getEventType();
+            TagEventRecord record = coldStore.getTagEvent(tagId, baiduAct);
+            if (record == null) {
+                record = coldStore.getTagEvent(tagId, 0);
+            }
+            if (record != null
+                    && record.getTencentActionType() != null
+                    && !record.getTencentActionType().isBlank()) {
+                return record.getTencentActionType();
             }
         } catch (Exception e) {
-            log.warn("[TagEventResolver] 查询 tencent_tag_event 失败 | tagId={} | error={}",
-                    tagId, e.getMessage());
+            log.warn("[TagEventResolver] 查询 tencent_tag_event 失败 | tagId={} | act={} | error={}",
+                    tagId, baiduAct, e.getMessage());
         }
 
         return null;
     }
+
+    private String buildRedisKey(String tagId, int baiduAct) {
+        return keyPrefix + tagId + ":" + baiduAct;
+    }
 }

+ 8 - 4
src/main/java/com/adx/tencent/tagsync/TagEventSyncService.java

@@ -16,8 +16,8 @@ import java.util.List;
 /**
  * 广告位回传方式同步服务。
  * 定时从 tencent_tag_event 表读取全部记录,同步到 Redis:
- *   key   = {prefix}{tag_id}(广告位ID)
- *   value = event_type(回传方式)
+ *   key   = {prefix}{tag_id}:{baidu_act}
+ *   value = tencent_action_type
  * 为避免表中删除的记录在 Redis 残留,写入时带 TTL(大于同步间隔)。
  */
 public class TagEventSyncService {
@@ -51,8 +51,8 @@ public class TagEventSyncService {
         redis.executePipelined((RedisConnection conn) -> {
             for (TagEventRecord row : rows) {
                 if (row.getTagId() == null || row.getTagId().isBlank()) continue;
-                byte[] key = (keyPrefix + row.getTagId()).getBytes(StandardCharsets.UTF_8);
-                byte[] val = (row.getEventType() == null ? "" : row.getEventType())
+                byte[] key = buildRedisKey(row.getTagId(), row.getBaiduAct()).getBytes(StandardCharsets.UTF_8);
+                byte[] val = (row.getTencentActionType() == null ? "" : row.getTencentActionType())
                         .getBytes(StandardCharsets.UTF_8);
                 if (ttlMs > 0) {
                     conn.stringCommands().set(key, val,
@@ -68,4 +68,8 @@ public class TagEventSyncService {
         log.info("[TagEventSync] 同步完成,共 {} 条广告位回传方式写入 Redis", rows.size());
         return rows.size();
     }
+
+    private String buildRedisKey(String tagId, int baiduAct) {
+        return keyPrefix + tagId + ":" + baiduAct;
+    }
 }

+ 31 - 8
src/main/java/com/adx/tencent/tencent/TencentClient.java

@@ -67,16 +67,12 @@ public class TencentClient {
      * @param bid                匹配到的竞价记录(含 callback URL)
      * @param payment            百度转化支付信息
      * @param platform           平台标识("android" 或 "ios")
-     * @param actionTypeOverride 广告位维度的回传方式;非空时优先作为 action_type,
-     *                           为空时回退到 act-map 映射
+     * @param actionType 已解析出的腾讯回传行为
      * @return MediaCallbackRecord 记录回传结果(含请求体、响应、状态)
      */
     public MediaCallbackRecord sendConversionEvent(BidRecord bid, PaymentInfo payment, String platform,
-                                                   String actionTypeOverride) {
+                                                   String actionType) {
         long eventTimeMs = Instant.now().toEpochMilli();
-        String actionType = (actionTypeOverride != null && !actionTypeOverride.isBlank())
-                ? actionTypeOverride
-                : conversionActionType(payment.getAct());
 
         MediaCallbackRecord record = new MediaCallbackRecord();
         record.setMedia("tencent");
@@ -231,7 +227,7 @@ public class TencentClient {
         UserAction action = new UserAction();
         action.setActionTime(eventTimeMs / 1000); // 秒级时间戳
         action.setActionType(actionType);
-        action.setOuterActionId(payment.getQk() + "_" + payment.getAct() + "_" + eventTimeMs);
+        action.setOuterActionId(buildOuterActionId(bid, payment, actionType));
 
         // 设备标识
         UserId userId = buildUserId(bid);
@@ -248,7 +244,8 @@ public class TencentClient {
         // action_param
         if ("PURCHASE".equals(actionType) || "COMPLETE_ORDER".equals(actionType)) {
             UserAction.ActionParam param = new UserAction.ActionParam();
-            param.setValue((int) (payment.getPayment() * 100)); // 分
+            double amount = payment.getGmv() > 0 ? payment.getGmv() : payment.getPayment();
+            param.setValue((int) (amount * 100)); // 分
             action.setActionParam(param);
         } else if ("START_APP".equals(actionType)) {
             // 留存行为必须带 length_of_stay(文档要求)
@@ -273,6 +270,21 @@ public class TencentClient {
         return action;
     }
 
+    private String buildOuterActionId(BidRecord bid, PaymentInfo payment, String actionType) {
+        String source = String.join("|",
+                safe(payment.getQk()),
+                safe(payment.getDate()),
+                safe(payment.getDeviceId()),
+                safe(bid.getTagId()),
+                String.valueOf(payment.getAct()),
+                safe(actionType));
+        return md5Hex(source);
+    }
+
+    private String safe(String value) {
+        return value == null ? "" : value;
+    }
+
     /**
      * 从 BidRecord 的 mediaParams 中提取设备标识构造 UserId。
      */
@@ -327,6 +339,17 @@ public class TencentClient {
         }
     }
 
+    private static String md5Hex(String value) {
+        if (value == null) return null;
+        try {
+            MessageDigest md = MessageDigest.getInstance("MD5");
+            byte[] hash = md.digest(value.getBytes(StandardCharsets.UTF_8));
+            return HexFormat.of().formatHex(hash);
+        } catch (Exception e) {
+            return value;
+        }
+    }
+
     private static String firstNotBlank(String... values) {
         for (String v : values) {
             if (v != null && !v.isBlank()) return v;

+ 8 - 4
src/main/resources/mapper/TagEventMapper.xml

@@ -4,14 +4,18 @@
 <mapper namespace="com.adx.tencent.storage.mapper.TagEventMapper">
     <resultMap id="tagEventResultMap" type="com.adx.tencent.storage.model.TagEventRecord">
         <result property="tagId" column="tag_id"/>
-        <result property="eventType" column="event_type"/>
+        <result property="baiduAct" column="baidu_act"/>
+        <result property="tencentActionType" column="tencent_action_type"/>
     </resultMap>
 
     <select id="selectAll" resultMap="tagEventResultMap">
-        SELECT tag_id, event_type FROM tencent_tag_event
+        SELECT tag_id, baidu_act, tencent_action_type
+        FROM tencent_tag_event
     </select>
 
-    <select id="selectByTagId" resultMap="tagEventResultMap">
-        SELECT tag_id, event_type FROM tencent_tag_event WHERE tag_id = #{tagId}
+    <select id="selectByTagIdAndAct" resultMap="tagEventResultMap">
+        SELECT tag_id, baidu_act, tencent_action_type
+        FROM tencent_tag_event
+        WHERE tag_id = #{tagId} AND baidu_act = #{baiduAct}
     </select>
 </mapper>