瀏覽代碼

fix : 解决监测链接匹配问题

bilingfeng 1 月之前
父節點
當前提交
0712add99d

+ 1 - 1
game-back/game-back-serve/src/main/java/com/zanxiang/game/back/serve/GameBackApplication.java

@@ -18,7 +18,7 @@ public class GameBackApplication {
 
     public static void main(String[] args) {
         SpringApplication.run(GameBackApplication.class, args);
-        System.out.println("腾讯广告新增监测链接注册回传(小游戏用户注册信息保存callBackUrl) (´・・)ノ(._.`)  \n" +
+        System.out.println("腾讯广告新增监测链接注册回传(解决监测链接匹配问题) (´・・)ノ(._.`)  \n" +
                 " ______  __     __     \n" +
                 "/_____/\\/__/\\ /__/\\    \n" +
                 "\\:::__\\/\\ \\::\\\\:.\\ \\   \n" +

+ 11 - 4
game-back/game-back-serve/src/main/java/com/zanxiang/game/back/serve/controller/TencentMiniGameLogController.java

@@ -8,10 +8,7 @@ import com.zanxiang.game.back.serve.pojo.dto.OrderReportDTO;
 import com.zanxiang.game.back.serve.pojo.vo.GameTencentMiniGameOrderVO;
 import com.zanxiang.game.back.serve.pojo.vo.GameTencentMiniGameUserVO;
 import com.zanxiang.game.back.serve.pojo.vo.GameTencentMiniOrderSplitLogVO;
-import com.zanxiang.game.back.serve.service.IGameTencentMiniGameOrderService;
-import com.zanxiang.game.back.serve.service.IGameTencentMiniGameOrderSplitLogService;
-import com.zanxiang.game.back.serve.service.IGameTencentMiniGameRoleRegisterService;
-import com.zanxiang.game.back.serve.service.IGameTencentMiniGameUserService;
+import com.zanxiang.game.back.serve.service.*;
 import com.zanxiang.module.util.exception.BaseException;
 import com.zanxiang.module.util.pojo.ResultVO;
 import io.swagger.annotations.Api;
@@ -37,6 +34,8 @@ public class TencentMiniGameLogController {
     private IGameTencentMiniGameOrderSplitLogService gameTencentMiniGameOrderSplitLogService;
     @Autowired
     private IGameTencentMiniGameRoleRegisterService gameTencentMiniGameRoleRegisterService;
+    @Autowired
+    private ITempCallbackService tempCallbackService;
 
     @PreAuthorize(permissionKey = "gameBack:tencentMiniGame:orderLogs")
     @PostMapping("/orderLogs")
@@ -111,4 +110,12 @@ public class TencentMiniGameLogController {
     public ResultVO<Boolean> tencentRoleReport(@PathVariable("ids") List<Long> ids) {
         return ResultVO.ok(gameTencentMiniGameRoleRegisterService.doReport(ids));
     }
+
+    @PreAuthorize(permissionKey = "gameBack:tencentMiniGame:checkCallBackUrl")
+    @PostMapping("/check/back/url")
+    @ApiOperation(value = "腾讯玩家注册回传地址检测")
+    public ResultVO<Boolean> checkCallBackUrl() {
+        tempCallbackService.checkCallBackUrl();
+        return ResultVO.ok();
+    }
 }

+ 12 - 0
game-back/game-back-serve/src/main/java/com/zanxiang/game/back/serve/dao/mapper/TempCallbackMapper.java

@@ -0,0 +1,12 @@
+package com.zanxiang.game.back.serve.dao.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.zanxiang.game.back.serve.pojo.entity.TempCallback;
+
+/**
+ * @author : lingfeng
+ * @time : 2025-03-06
+ * @description : 临时表
+ */
+public interface TempCallbackMapper extends BaseMapper<TempCallback> {
+}

+ 210 - 0
game-back/game-back-serve/src/main/java/com/zanxiang/game/back/serve/pojo/entity/TempCallback.java

@@ -0,0 +1,210 @@
+package com.zanxiang.game.back.serve.pojo.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+import java.time.LocalDate;
+
+/**
+ * @author : lingfeng
+ * @time : 2025-03-06
+ * @description : 腾讯小游戏监测链接数据临时表
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+@TableName(TempCallback.TABLE_NAME)
+public class TempCallback implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+    public static final String TABLE_NAME = "t_temp_callback";
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    private String agentKey;
+
+    private Long gameId;
+
+    /**
+     * 时间发生日期
+     */
+    private LocalDate day;
+
+    /**
+     * 点击id, ex: 24oi6xq2aaakvagnqu7a, 宏: __CLICK_ID__
+     */
+    private String clickId;
+
+    /**
+     * 点击时间, ex: 1586437362, 宏: __CLICK_TIME__
+     */
+    private Long clickTime;
+
+    /**
+     * 曝光时间, ex: 1586437361, ex: , 宏: __IMPRESSION_TIME__
+     */
+    private Long impressionTime;
+
+    /**
+     * 广告组id(实际为广告id), ex: 228691429, ex: , 宏: __ADGROUP_ID__
+     */
+    private Long adgroupId;
+
+    /**
+     * 创意 ID, ex: 654321, 宏: __DYNAMIC_CREATIVE_ID__
+     */
+    private Long dynamicCreativeId;
+
+    /**
+     * 营销资产ID, ex: 12345, 宏: __MARKETING_ASSET_ID__
+     */
+    private Long marketingAssetId;
+
+    /**
+     * 素材标签ID, 宏: __MATERIAL_PACKAGE_ID__
+     */
+    private String materialPackageId;
+
+    /**
+     * 广告投放平台, 1:GDT entrance、3:京东直投、5:经 wechat mp 投放的广告、6:京东运营、8:QQ 公众账号平台、9:移动联盟 SSP、10:58 运营、11:58 商户、12:易车运营、13:易车商户、14:融 360 运营、15:融 360 商户、16:点评运营、17:点评商户、18:来自 OMG 的广告主、19:京东外单, 宏: __AD_PLATFORM_TYPE__
+     */
+    private Integer adPlatformType;
+
+    /**
+     * 广告主id, ex: 9471147, 宏: __ACCOUNT_ID__
+     */
+    private Long accountId;
+
+    /**
+     * 代理商id, ex: 1050262, 宏: __AGENCY_ID__
+     */
+    private Long agencyId;
+
+    /**
+     * 点击sku, ex: 478c4a93a054f7c9087b4ecb1f03f8a1, 宏: __CLICK_SKU_ID__
+     */
+    private String clickSkuId;
+
+    /**
+     * 设备类型, ex: ios、android, 宏: __DEVICE_OS_TYPE__
+     */
+    private String deviceOsType;
+
+    /**
+     * 请求时间, ex: 1586437335, 宏: __PROCESS_TIME__
+     */
+    private Long processTime;
+
+    /**
+     * 应用id, ex: 1101072624、wx69618ae091cf2c76, 宏: __PROMOTED_OBJECT_ID__
+     */
+    private String promotedObjectId;
+
+    /**
+     * 请求id, ex: vqp7xdombqonw, 宏: __REQUEST_ID__
+     */
+    private String requestId;
+
+    /**
+     * 曝光id, ex: xkrx5et47h7g401, 宏: __IMPRESSION_ID__
+     */
+    private String impressionId;
+
+    /**
+     * 设备id(imei或idfa的加密值),对 IMEI 设备号转成小写,再进行md5编码,再小写,32位、对 IDFA 设备号保持大写,再进行 md5 编码,再小写,32位, 宏: __MUID__
+     */
+    private String muid;
+
+    /**
+     * 安卓id做md5加密后小写, ex: 797745b011e3286de9e1a1c59ba72c97, 宏: __HASH_ANDROID_ID__
+     */
+    private String hashAndroidId;
+
+    /**
+     * 媒体投放系统获取的用户终端的公共IPV4地址, ex: 183.226.102.120, 宏: __IP__
+     */
+    private String ip;
+
+    /**
+     * 用户代理(user_agent), ex: Dalvik%2F2.1.0+%28Linux%3B+U%3B+Android+8.0.0%3B+PIC-AL00+Build%2FHUAWEIPIC-AL00%29, 宏: __USER_AGENT__
+     */
+    private String userAgent;
+
+    /**
+     * 情况1:使用 DataNexus 配置,并与广告直接绑定(ex: 空值)、情况2:新版转化归因中的监测链接信息(使用 DataNexus 或直接填写监测链接)直接提供上报信息回传接口的 url,示例为url encode 编码原值,广告主需要 decode 作为 post 请求url回传至腾讯广告(ex: http%3A%2F%2Ftracking.e.qq.com%2Fconv%3Fcb%3DxXx%252BxXx%253D%26conv_id%3D123)、情况3:使用投放管理平台 - 工具(ex: http://tracking.e.qq.com/conv?cb=%s&&%s&&%s&&%s 其中%s代表的参数分别为 productId、productType、advertiserId、clickId), 宏: __CALLBACK__
+     */
+    private String callback;
+
+    /**
+     * 联盟广告位id, ex: 8144201, 宏: __ENCRYPTED_POSITION_ID__
+     */
+    private String encryptedPositionId;
+
+    /**
+     * 媒体投放系统获取的用户终端的公共IPV6地址, ex: 2409%3A8a55%3A4cc0%3A4050%3A2507%3A4922%3Abbe0%3A524b, 宏: __IPV6__
+     */
+    private String ipv6;
+
+    /**
+     * Android Q 及更高版本的设备号,64位及以下,取原值后做md5加密, ex: 9d271e4d04de7e4b0b4f1df20e79ce64, 宏: __HASH_OAID__
+     */
+    private String hashOaid;
+
+    /**
+     * URL Encode后的JSON数组;其中qaid为中广协ID(即CAID),hash_qaid为CAID原值MD5加密后的结果, version为腾讯版本号,支持两个版本同时下发(即最新版和上一版),腾讯版本号与中广协版本对应关系为:腾讯 1001 = 中广协 20200901;腾讯 1003 = 中广协 20201230; 腾讯1004 = 中广协 20211207;腾讯1005=中广协 20220111;腾讯1006=中广协 20230330, 宏: __QAID_CAA__
+     */
+    private String caid;
+
+    /**
+     * 机型, ex:  PCKM00、Redmi 7、iPhone 7..., 宏: __MODEL__
+     */
+    private String model;
+
+    /**
+     * 专用于网页类小程序转化规则的点击监测下发,其它类型不支持该字段下发。每个用户针对小程序应用会产生一个安全的OpenID,只针对当前的小程序有效, ex: ozWH25VK0aodxYMZrX0Lqj9HHhrg, 宏: __WECHAT_OPEN_ID__
+     */
+    private String wechatOpenid;
+
+    /**
+     * 媒体投放系统获取的用户终端的公共IPV4地址MD5加密后转小写,仅在新版转化里支持配置, 宏: __IP_MD5__
+     */
+    private String ipMd5;
+
+    /**
+     * 媒体投放系统获取的用户终端的公共IPV6地址MD5加密后转小写,仅在新版转化里支持配置, 宏: __IPV6_MD5__
+     */
+    private String ipv6Md5;
+
+    /**
+     * 渠道包id, 只 for Android 设备生效, 宏: __CHANNEL_PACKAGE_ID__
+     */
+    private String channelPackageId;
+
+    /**
+     * 操作系统版本, 只 for iOS 和 Android 设备生效, 宏: __DEVICE_OS_VERSION__
+     */
+    private String deviceOsVersion;
+
+    /**
+     * 行为类型, LANDING_PAGE_CLICK:点击跳转按钮,RESERVATION:表单预约, 宏: __ACT_TYPE__
+     */
+    private String actType;
+
+    /**
+     * 行为时间, ex: 1586437361, 宏: __ACT_TIME__
+     */
+    private Long actTime;
+
+    /**
+     * 微信小游戏appid
+     */
+    private String wechatAppId;
+}

+ 2 - 1
game-back/game-back-serve/src/main/java/com/zanxiang/game/back/serve/rpc/impl/TencentMiniGameBackRpcImpl.java

@@ -235,7 +235,8 @@ public class TencentMiniGameBackRpcImpl implements ITencentMiniGameBackRpc {
                 .backType(BackTypeEnum.BACK_API.getBackType())
                 .build();
         //匹配回传URL
-        String callBackUrl = gameTencentMiniGameCallbackService.getCallBackUrl(dto.getWechatOpenid(), dto.getClickId());
+        String callBackUrl = gameTencentMiniGameCallbackService.getCallBackUrl(dto.getGameId(), dto.getWechatOpenid(),
+                dto.getClickId(), dto.getRegisterTime());
         if (Strings.isNotBlank(callBackUrl)) {
             userLog.setCallBackUrl(callBackUrl);
         }

+ 3 - 1
game-back/game-back-serve/src/main/java/com/zanxiang/game/back/serve/service/IGameTencentMiniGameCallbackService.java

@@ -4,9 +4,11 @@ import com.baomidou.mybatisplus.extension.service.IService;
 import com.zanxiang.game.back.serve.pojo.dto.GameTencentCallbackDTO;
 import com.zanxiang.game.back.serve.pojo.entity.GameTencentMiniGameCallback;
 
+import java.time.LocalDateTime;
+
 public interface IGameTencentMiniGameCallbackService extends IService<GameTencentMiniGameCallback> {
 
     boolean callback(GameTencentCallbackDTO dto);
 
-    String getCallBackUrl(String wechatOpenid, String clickId);
+    String getCallBackUrl(Long gameId, String wechatOpenid, String clickId, LocalDateTime registerTime);
 }

+ 14 - 0
game-back/game-back-serve/src/main/java/com/zanxiang/game/back/serve/service/ITempCallbackService.java

@@ -0,0 +1,14 @@
+package com.zanxiang.game.back.serve.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.zanxiang.game.back.serve.pojo.entity.TempCallback;
+
+/**
+ * @author : lingfeng
+ * @time : 2025-03-06
+ * @description : 临时表
+ */
+public interface ITempCallbackService extends IService<TempCallback> {
+
+    void checkCallBackUrl();
+}

+ 27 - 5
game-back/game-back-serve/src/main/java/com/zanxiang/game/back/serve/service/impl/GameTencentMiniGameCallbackServiceImpl.java

@@ -23,6 +23,7 @@ import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
 
 import java.time.LocalDate;
+import java.time.LocalDateTime;
 import java.util.Optional;
 
 @Slf4j
@@ -66,28 +67,49 @@ public class GameTencentMiniGameCallbackServiceImpl extends ServiceImpl<GameTenc
     }
 
     @Override
-    public String getCallBackUrl(String wechatOpenid, String clickId) {
+    public String getCallBackUrl(Long gameId, String wechatOpenid, String clickId, LocalDateTime registerTime) {
         try {
+            //1. 精准匹配, openId 必须匹配, requestId, clickId, impressionId, 有一个匹配上即可
             GameTencentMiniGameCallback tencentMiniGameCallback = super.getOne(
                     new LambdaQueryWrapper<GameTencentMiniGameCallback>()
+                            .eq(GameTencentMiniGameCallback::getGameId, gameId)
                             .eq(GameTencentMiniGameCallback::getWechatOpenid, wechatOpenid)
                             .and(qw -> qw.eq(GameTencentMiniGameCallback::getImpressionId, clickId)
                                     .or().eq(GameTencentMiniGameCallback::getRequestId, clickId)
-                                    .or().eq(GameTencentMiniGameCallback::getClickId, clickId)
-                            )
+                                    .or().eq(GameTencentMiniGameCallback::getClickId, clickId))
                             .orderByDesc(GameTencentMiniGameCallback::getClickTime)
                             .last("limit 1"));
+            //2. 模糊匹配, 分别使用 requestId, clickId, impressionId 匹配, 有一个匹配上即可, 加上注册时间距离最近条件
+            if (tencentMiniGameCallback == null) {
+                tencentMiniGameCallback = super.getOne(new LambdaQueryWrapper<GameTencentMiniGameCallback>()
+                        .eq(GameTencentMiniGameCallback::getGameId, gameId)
+                        .and(qw -> qw.eq(GameTencentMiniGameCallback::getImpressionId, clickId)
+                                .or().eq(GameTencentMiniGameCallback::getRequestId, clickId)
+                                .or().eq(GameTencentMiniGameCallback::getClickId, clickId))
+                        .le(GameTencentMiniGameCallback::getClickTime, DateUtil.localDateTimeToSecond(registerTime))
+                        .orderByDesc(GameTencentMiniGameCallback::getClickTime)
+                        .last("limit 1"));
+            }
+            //3. 兜底逻辑, 只用openId匹配
+            if (tencentMiniGameCallback == null) {
+                tencentMiniGameCallback = super.getOne(new LambdaQueryWrapper<GameTencentMiniGameCallback>()
+                        .eq(GameTencentMiniGameCallback::getGameId, gameId)
+                        .eq(GameTencentMiniGameCallback::getWechatOpenid, wechatOpenid)
+                        .orderByDesc(GameTencentMiniGameCallback::getClickTime)
+                        .last("limit 1"));
+            }
+            //判断监测链接数据
             String callBack = Optional.ofNullable(tencentMiniGameCallback)
                     .map(GameTencentMiniGameCallback::getCallback)
                     .orElse(null);
             if (Strings.isBlank(callBack)) {
-                log.error("匹配监测链接为空, wechatOpenid : {}, clickId : {}", wechatOpenid, clickId);
+                log.error("匹配监测链接为空, gameId : {}, wechatOpenid : {}, clickId : {}", gameId, wechatOpenid, clickId);
                 return null;
             }
             //返回匹配回传地址
             return URIUtil.decodeURIComponent(callBack);
         } catch (Exception e) {
-            log.error("匹配监测链接异常, wechatOpenid : {}, clickId : {}", wechatOpenid, clickId);
+            log.error("匹配监测链接异常, gameId : {}, wechatOpenid : {}, clickId : {}", gameId, wechatOpenid, clickId);
         }
         return null;
     }

+ 117 - 0
game-back/game-back-serve/src/main/java/com/zanxiang/game/back/serve/service/impl/TempCallbackServiceImpl.java

@@ -0,0 +1,117 @@
+package com.zanxiang.game.back.serve.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.zanxiang.game.back.serve.dao.mapper.TempCallbackMapper;
+import com.zanxiang.game.back.serve.pojo.entity.GameTencentMiniGameUser;
+import com.zanxiang.game.back.serve.pojo.entity.TempCallback;
+import com.zanxiang.game.back.serve.service.IGameTencentMiniGameUserService;
+import com.zanxiang.game.back.serve.service.ITempCallbackService;
+import com.zanxiang.module.util.DateUtil;
+import com.zanxiang.module.util.URIUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.logging.log4j.util.Strings;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * @author : lingfeng
+ * @time : 2025-03-06
+ * @description : 临时表
+ */
+@Slf4j
+@Service
+public class TempCallbackServiceImpl extends ServiceImpl<TempCallbackMapper, TempCallback> implements ITempCallbackService {
+
+    @Autowired
+    private IGameTencentMiniGameUserService gameTencentMiniGameUserService;
+
+    @Override
+    public void checkCallBackUrl() {
+        long current = 1;
+        long size = 5000;
+        long pages;
+        do {
+            Page<GameTencentMiniGameUser> miniGameUserPage = gameTencentMiniGameUserService.page(Page.of(current, size),
+                    new LambdaQueryWrapper<GameTencentMiniGameUser>()
+                            .select(GameTencentMiniGameUser::getId, GameTencentMiniGameUser::getWechatOpenid,
+                                    GameTencentMiniGameUser::getWechatOpenid)
+                            .eq(GameTencentMiniGameUser::getGameId, 35L)
+                            .eq(GameTencentMiniGameUser::getBackType, "BACK_API")
+                            .isNull(GameTencentMiniGameUser::getCallBackUrl)
+                            .orderByDesc(GameTencentMiniGameUser::getCreateTime));
+            pages = miniGameUserPage.getPages();
+            log.error("第 {} 页正在处理-------> , 共 {} 页", current, pages);
+            this.updateCallBackUrlBatch(miniGameUserPage.getRecords());
+            log.error("第 {} 处理完成 =========> , 共 {} 页", current, pages);
+        } while (++current <= pages);
+    }
+
+    private void updateCallBackUrlBatch(List<GameTencentMiniGameUser> records) {
+        if (CollectionUtils.isEmpty(records)) {
+            return;
+        }
+        records.forEach(r -> {
+            String callBackUrl = this.getCallBackUrl(r.getGameId(), r.getWechatOpenid(), r.getClickId(), r.getRegisterTime());
+            if (Strings.isBlank(callBackUrl)) {
+                return;
+            }
+            gameTencentMiniGameUserService.update(new LambdaUpdateWrapper<GameTencentMiniGameUser>()
+                    .set(GameTencentMiniGameUser::getCallBackUrl, callBackUrl)
+                    .eq(GameTencentMiniGameUser::getId, r.getId())
+            );
+        });
+    }
+
+    private String getCallBackUrl(Long gameId, String wechatOpenid, String clickId, LocalDateTime registerTime) {
+        try {
+            //1. 精准匹配, openId 必须匹配, requestId, clickId, impressionId, 有一个匹配上即可
+            TempCallback tempCallback = super.getOne(new LambdaQueryWrapper<TempCallback>()
+                    .eq(TempCallback::getGameId, gameId)
+                    .eq(TempCallback::getWechatOpenid, wechatOpenid)
+                    .and(qw -> qw.eq(TempCallback::getImpressionId, clickId)
+                            .or().eq(TempCallback::getRequestId, clickId)
+                            .or().eq(TempCallback::getClickId, clickId))
+                    .orderByDesc(TempCallback::getClickTime)
+                    .last("limit 1"));
+            //2. 模糊匹配, 分别使用 requestId, clickId, impressionId 匹配, 有一个匹配上即可, 加上注册时间距离最近条件
+            if (tempCallback == null) {
+                tempCallback = super.getOne(new LambdaQueryWrapper<TempCallback>()
+                        .eq(TempCallback::getGameId, gameId)
+                        .and(qw -> qw.eq(TempCallback::getImpressionId, clickId)
+                                .or().eq(TempCallback::getRequestId, clickId)
+                                .or().eq(TempCallback::getClickId, clickId))
+                        .le(TempCallback::getClickTime, DateUtil.localDateTimeToSecond(registerTime))
+                        .orderByDesc(TempCallback::getClickTime)
+                        .last("limit 1"));
+            }
+            //3. 兜底逻辑, 只用openId匹配
+            if (tempCallback == null) {
+                tempCallback = super.getOne(new LambdaQueryWrapper<TempCallback>()
+                        .eq(TempCallback::getGameId, gameId)
+                        .eq(TempCallback::getWechatOpenid, wechatOpenid)
+                        .orderByDesc(TempCallback::getClickTime)
+                        .last("limit 1"));
+            }
+            String callBack = Optional.ofNullable(tempCallback)
+                    .map(TempCallback::getCallback)
+                    .orElse(null);
+            if (Strings.isBlank(callBack)) {
+                log.error("匹配监测链接为空, wechatOpenid : {}, clickId : {}", wechatOpenid, clickId);
+                return null;
+            }
+            //返回匹配回传地址
+            return URIUtil.decodeURIComponent(callBack);
+        } catch (Exception e) {
+            log.error("匹配监测链接异常, wechatOpenid : {}, clickId : {}", wechatOpenid, clickId);
+        }
+        return null;
+    }
+}