Просмотр исходного кода

Merge branch 'cooperation_video_candidate_pool_improved_lld_0509' into test

刘立冬 7 часов назад
Родитель
Сommit
5c953faf83

+ 1 - 1
api-module/src/main/java/com/tzld/piaoquan/api/component/ManagerApiService.java

@@ -184,7 +184,7 @@ public class ManagerApiService {
         try {
             JSONObject param = new JSONObject();
             param.put("queryText", queryText);
-            param.put("configCode", "ALL");
+            param.put("configCode", "VIDEO_TITLE");
             param.put("topN", topN);
             param.put("simMin", 0.8);
             String post = httpPoolClient.post(url, param.toJSONString());

+ 17 - 6
api-module/src/main/java/com/tzld/piaoquan/api/component/TouLiuHttpClient.java

@@ -10,6 +10,7 @@ import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
+import java.net.URLEncoder;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -35,10 +36,6 @@ public class TouLiuHttpClient {
         String url = baseUrl + "/ad/put/flow/add/tencent";
         UUID uuid = UUID.randomUUID();
         String experimentIdParam = (experimentId != null && !experimentId.isEmpty()) ? "&experimentId=" + experimentId : "";
-        String growthExtDataParam = "";
-        if (experimentId != null && !experimentId.isEmpty()) {
-            growthExtDataParam = "&growthExtData={\"experimentId\":\"" + experimentId + "\"}";
-        }
         String experimentIdField = (experimentId != null && !experimentId.isEmpty()) ? "\"experimentId\":\"" + experimentId + "\"," : "";
         String jsonBody = "{" +
                 "\"videoId\":\"" + videoId + "\"," +
@@ -52,11 +49,25 @@ public class TouLiuHttpClient {
                 experimentIdField +
                 "\"path\":\"" + "pages/category" + "\"," +
                 "\"requestParam\":{" +
-                "\"jumpPage\":\"" + "pages/user-videos?fromGzh=1&rootShareId=" + uuid + "&id=" + videoId + "&shareId=" + uuid + "&rootSourceId=[rootSourceId]" + experimentIdParam + growthExtDataParam + "\"" +
+                "\"jumpPage\":\"" + "pages/user-videos?fromGzh=1&rootShareId=" + uuid + "&id=" + videoId + "&shareId=" + uuid + "&rootSourceId=[rootSourceId]" + experimentIdParam + "\"" +
                 "}" +
                 "}";
         try {
-            return httpPoolClient.post(url, jsonBody);
+            String response = httpPoolClient.post(url, jsonBody);
+            // 在返回的url字段后拼接growthExtData参数(URL编码)
+            if (experimentId != null && !experimentId.isEmpty() && response != null && !response.isEmpty()) {
+                JSONObject responseJson = JSONObject.parseObject(response);
+                JSONObject data = responseJson.getJSONObject("data");
+                if (data != null && data.getString("url") != null) {
+                    String pageUrl = data.getString("url");
+                    String growthExtDataJson = "{\"experimentId\":\"" + experimentId + "\"}";
+                    pageUrl = pageUrl + "%26growthExtData%3D" + URLEncoder.encode(growthExtDataJson, "UTF-8");
+                    data.put("url", pageUrl);
+                    responseJson.put("data", data);
+                    return responseJson.toJSONString();
+                }
+            }
+            return response;
         } catch (Exception e) {
             log.error("sendAdFlowAddRequest error", e);
         }

+ 29 - 0
api-module/src/main/java/com/tzld/piaoquan/api/dao/mapper/contentplatform/ext/ContentPlatformPlanMapperExt.java

@@ -4,6 +4,7 @@ import com.tzld.piaoquan.api.model.param.contentplatform.GzhPlanListParam;
 import com.tzld.piaoquan.api.model.param.contentplatform.QwPlanListParam;
 import com.tzld.piaoquan.api.model.param.contentplatform.VideoContentListParam;
 import com.tzld.piaoquan.api.model.po.contentplatform.*;
+import com.tzld.piaoquan.api.model.vo.contentplatform.VideoContentItemVO;
 import org.apache.ibatis.annotations.Param;
 
 import java.util.List;
@@ -133,4 +134,32 @@ public interface ContentPlatformPlanMapperExt {
                                               @Param("now") Long now);
 
     List<ContentPlatformGzhPlanVideo> getGzhPushPlanVideoByRootSourceId(@Param("rootSourceId") String rootSourceId, @Param("planType") Integer planType);
+
+    Integer getSelectedQwVideoCount(@Param("loginAccountId") Long loginAccountId,
+                                    @Param("planType") Integer planType,
+                                    @Param("title") String title);
+
+    List<VideoContentItemVO> getSelectedQwVideoList(@Param("loginAccountId") Long loginAccountId,
+                                                    @Param("planType") Integer planType,
+                                                    @Param("title") String title,
+                                                    @Param("offset") int offset,
+                                                    @Param("pageSize") Integer pageSize);
+
+    Integer getSelectedGzhVideoCount(@Param("loginAccountId") Long loginAccountId,
+                                     @Param("planType") Integer planType,
+                                     @Param("title") String title);
+
+    List<VideoContentItemVO> getSelectedGzhVideoList(@Param("loginAccountId") Long loginAccountId,
+                                                     @Param("planType") Integer planType,
+                                                     @Param("title") String title,
+                                                     @Param("offset") int offset,
+                                                     @Param("pageSize") Integer pageSize);
+
+    List<Long> getUserPublishedQwVideoIds(@Param("loginAccountId") Long loginAccountId,
+                                          @Param("planType") Integer planType,
+                                          @Param("limit") Integer limit);
+
+    List<Long> getUserPublishedGzhVideoIds(@Param("loginAccountId") Long loginAccountId,
+                                           @Param("planType") Integer planType,
+                                           @Param("limit") Integer limit);
 }

+ 1 - 1
api-module/src/main/java/com/tzld/piaoquan/api/job/contentplatform/ContentPlatformDemandVideoJob.java

@@ -270,7 +270,7 @@ public class ContentPlatformDemandVideoJob {
         }
         for (List<Long> partition : Lists.partition(videoIds, ODPS_CATEGORY_CHUNK)) {
             String inClause = partition.stream().map(String::valueOf).collect(Collectors.joining(","));
-            String sql = "SELECT videoid, merge_leve2 FROM loghubods.video_merge_tag WHERE videoid IN (" + inClause + ")";
+            String sql = "SELECT videoid, merge_leve2 FROM loghubods.video_merge_tag WHERE videoid IN (" + inClause + ");";
             try {
                 List<Record> records = odpsManager.query(sql);
                 if (CollectionUtils.isEmpty(records)) {

+ 8 - 1
api-module/src/main/java/com/tzld/piaoquan/api/model/param/contentplatform/VideoContentListParam.java

@@ -1,9 +1,12 @@
 package com.tzld.piaoquan.api.model.param.contentplatform;
 
+import com.fasterxml.jackson.annotation.JsonIgnore;
 import com.tzld.piaoquan.api.model.param.PageParam;
 import io.swagger.annotations.ApiModelProperty;
 import lombok.Data;
 
+import java.util.List;
+
 @Data
 public class VideoContentListParam extends PageParam {
 
@@ -19,9 +22,13 @@ public class VideoContentListParam extends PageParam {
     @ApiModelProperty(value = "排序 0-平台推荐 1-行业裂变率 2-本渠道裂变率 3-推荐指数")
     private Integer sort = 0;
 
-    @ApiModelProperty(value = "数据来源: prior-人群需求 / posterior-优质相似 / hot-全局热门 / 空-全部穿插")
+    @ApiModelProperty(value = "数据来源: prior-人群需求 / posterior-优质相似 / hot-全局热门 / selected-历史已选 / 空-全部穿插")
     private String source;
 
     @ApiModelProperty(value = "公众号名称(对应 demand.channel_level3),仅 prior/posterior 路使用,无数据时退化为渠道粒度")
     private String ghName;
+
+    /** 内部使用:当前登录账号在当前 type 下已发布过的 video_id 子集,用于在 hot/prior/posterior 池里 NOT IN 排除。不暴露给前端,JSON 反序列化时也忽略。 */
+    @JsonIgnore
+    private List<Long> excludeVideoIds;
 }

+ 134 - 1
api-module/src/main/java/com/tzld/piaoquan/api/service/contentplatform/impl/ContentPlatformPlanServiceImpl.java

@@ -630,6 +630,10 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
     private static final String SOURCE_PRIOR = "prior";
     private static final String SOURCE_POSTERIOR = "posterior";
     private static final String SOURCE_HOT = "hot";
+    private static final String SOURCE_SELECTED = "selected";
+    /** 单用户历史已发布 video_id 上限:超过 500 条的老视频不再参与 NOT IN 过滤,
+     * 控制 SQL 体积,老视频本来曝光机会就低,丢失精度可接受。 */
+    private static final int EXCLUDE_PUBLISHED_LIMIT = 500;
 
     /**
      * 推导 channel_name(人群_渠道) 作为 demand 池强过滤。
@@ -654,11 +658,23 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
     @Override
     public Page<VideoContentItemVO> getVideoContentList(VideoContentListParam param) {
         ContentPlatformAccount user = LoginUserContext.getUser();
+        String source = param.getSource();
+        // 历史已选自带 title 模糊搜索,优先于通用 title 向量搜索分发
+        if (SOURCE_SELECTED.equalsIgnoreCase(source)) {
+            return getSelectedSourcePaged(param, user);
+        }
+        // 其他 source(含 title 向量搜索路径不参与):预查询当前账号历史已发布 video_id,
+        // 用于 hot/prior/posterior 池排除,避免重复曝光。
+        if (user != null && user.getId() != null && !StringUtils.hasText(param.getTitle())) {
+            List<Long> excludeVideoIds = getUserPublishedVideoIds(user.getId(), param.getType());
+            if (CollectionUtils.isNotEmpty(excludeVideoIds)) {
+                param.setExcludeVideoIds(excludeVideoIds);
+            }
+        }
         // 如果 title 有内容,调用 manager 平台接口搜索
         if (StringUtils.hasText(param.getTitle())) {
             return getVideoContentListByTitleV2(param);
         }
-        String source = param.getSource();
         if (SOURCE_PRIOR.equalsIgnoreCase(source)) {
             return getSingleSourcePage(param, user, SOURCE_PRIOR);
         }
@@ -671,6 +687,114 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
         return getInterleavedPage(param, user);
     }
 
+    /**
+     * 历史已选:返回当前登录账号在当前入口/type 下已经发布过的视频(按 video_id 去重,取最近一次).
+     * 按入口隔离 — 企微入口查 qw_plan_video,公众号入口查 gzh_plan_video.
+     * 排序:MAX(create_timestamp) DESC(最近发布在前).
+     * type 未指定/不在已知范围 → 返回空,前端引导用户先选行业.
+     */
+    private Page<VideoContentItemVO> getSelectedSourcePaged(VideoContentListParam param, ContentPlatformAccount user) {
+        int pageSize = param.getPageSize();
+        int pageNum = param.getPageNum();
+        Page<VideoContentItemVO> result = new Page<>(pageNum, pageSize);
+        if (user == null || user.getId() == null) {
+            result.setTotalSize(0);
+            result.setObjs(new ArrayList<>());
+            return result;
+        }
+        Integer type = param.getType();
+        Long loginAccountId = user.getId();
+        String title = StringUtils.hasText(param.getTitle()) ? param.getTitle() : null;
+        int offset = (pageNum - 1) * pageSize;
+
+        Integer qwPlanType = mapToQwPlanType(type);
+        Integer gzhPlanType = mapToGzhPlanType(type);
+
+        int count;
+        List<VideoContentItemVO> list;
+        if (qwPlanType != null) {
+            count = nullToZero(planMapperExt.getSelectedQwVideoCount(loginAccountId, qwPlanType, title));
+            result.setTotalSize(count);
+            if (count == 0 || offset >= count) {
+                result.setObjs(new ArrayList<>());
+                return result;
+            }
+            list = planMapperExt.getSelectedQwVideoList(loginAccountId, qwPlanType, title, offset, pageSize);
+        } else if (gzhPlanType != null) {
+            count = nullToZero(planMapperExt.getSelectedGzhVideoCount(loginAccountId, gzhPlanType, title));
+            result.setTotalSize(count);
+            if (count == 0 || offset >= count) {
+                result.setObjs(new ArrayList<>());
+                return result;
+            }
+            list = planMapperExt.getSelectedGzhVideoList(loginAccountId, gzhPlanType, title, offset, pageSize);
+        } else {
+            // type 缺失或未知:历史已选不知道走哪张表,直接返回空
+            result.setTotalSize(0);
+            result.setObjs(new ArrayList<>());
+            return result;
+        }
+        if (list == null) {
+            list = new ArrayList<>();
+        }
+        for (VideoContentItemVO v : list) {
+            v.setSource(SOURCE_SELECTED);
+        }
+        result.setObjs(list);
+        return result;
+    }
+
+    /**
+     * 取当前登录账号在指定 type 入口下已发布的 video_id(最多 EXCLUDE_PUBLISHED_LIMIT 条,按发布时间倒序).
+     * type 无法映射时返回空,等同于不过滤.
+     */
+    private List<Long> getUserPublishedVideoIds(Long loginAccountId, Integer type) {
+        Integer qwPlanType = mapToQwPlanType(type);
+        if (qwPlanType != null) {
+            return planMapperExt.getUserPublishedQwVideoIds(loginAccountId, qwPlanType, EXCLUDE_PUBLISHED_LIMIT);
+        }
+        Integer gzhPlanType = mapToGzhPlanType(type);
+        if (gzhPlanType != null) {
+            return planMapperExt.getUserPublishedGzhVideoIds(loginAccountId, gzhPlanType, EXCLUDE_PUBLISHED_LIMIT);
+        }
+        return Collections.emptyList();
+    }
+
+    /** 前端 param.type → qw_plan.type;非企微入口返回 null. */
+    private Integer mapToQwPlanType(Integer type) {
+        if (type == null) return null;
+        if (type == 2) return QwPlanTypeEnum.GROUP.getVal();
+        if (type == 3) return QwPlanTypeEnum.REPLY.getVal();
+        return null;
+    }
+
+    /** 前端 param.type → gzh_plan.type;非公众号入口返回 null. */
+    private Integer mapToGzhPlanType(Integer type) {
+        if (type == null) return null;
+        if (type == 0) return ContentPlatformGzhPlanTypeEnum.AUTO_REPLY.getVal();
+        if (type == 1) return ContentPlatformGzhPlanTypeEnum.FWH_PUSH.getVal();
+        if (type == 4) return ContentPlatformGzhPlanTypeEnum.GZH_PUSH.getVal();
+        return null;
+    }
+
+    private int nullToZero(Integer v) {
+        return v == null ? 0 : v;
+    }
+
+    /**
+     * 内存过滤 demand 池行,剔除当前用户已发布的 video_id.
+     * 在 demand pipeline 早期调用,避免被剔除的视频占用 groupAndTopK 名额.
+     */
+    private List<ContentPlatformDemandVideo> excludePublishedRows(List<ContentPlatformDemandVideo> rows, List<Long> excludeVideoIds) {
+        if (CollectionUtils.isEmpty(rows) || CollectionUtils.isEmpty(excludeVideoIds)) {
+            return rows;
+        }
+        Set<Long> excludeSet = new HashSet<>(excludeVideoIds);
+        return rows.stream()
+                .filter(r -> r.getVideoId() == null || !excludeSet.contains(r.getVideoId()))
+                .collect(Collectors.toList());
+    }
+
     /**
      * 单一来源分页:与穿插使用同一套候选构建逻辑(人群需求/优质相似各 2 阶段、组内 score top K),
      * 再按 pageNum/pageSize 在内存中分页。totalSize = 去重后总数。
@@ -883,6 +1007,8 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
             rows = demandVideoMapperExt.selectForRecommend(
                     dt, channelName, null, DEMAND_STRATEGY_PRIOR_SCENE, null, null, null, null, null, category, limit, false);
         }
+        // 0. 历史已选剔除:用户已发布过的 video_id 不再出现在 priorScene 池中
+        rows = excludePublishedRows(rows, param.getExcludeVideoIds());
         // 1. 同 video_id 取 total_rov 最大的代表行(SQL 已排序,putIfAbsent 保留首次)
         LinkedHashMap<Long, ContentPlatformDemandVideo> bestPerVideo = new LinkedHashMap<>();
         for (ContentPlatformDemandVideo r : rows) {
@@ -941,6 +1067,9 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
                     dt, channelName, null, DEMAND_STRATEGY_PRIOR, PRIOR_PREMIUM_DIMENSION, null, null, null, null, category, fetchLimit, false);
         }
 
+        // 历史已选剔除:在 rov 过滤和分组裁剪前剔除,避免被排除的视频占用 topK 名额
+        rows = excludePublishedRows(rows, param.getExcludeVideoIds());
+
         // 近 7 日 rov 下限,过滤掉低质量近期表现的视频(0513 验证 ≥0.02 保留 ~41%)
         rows = rows.stream()
                 .filter(r -> r.getRov() != null && r.getRov() >= DEMAND_MIN_ROV)
@@ -1023,6 +1152,9 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
                     dt, channelName, null, DEMAND_STRATEGY_POSTERIOR, null, null, null, null, null, category, fetchLimit, true);
         }
 
+        // 历史已选剔除:在 rov 过滤和分组裁剪前剔除,避免被排除的视频占用 topK 名额
+        rows = excludePublishedRows(rows, param.getExcludeVideoIds());
+
         // 近 7 日 rov 下限,与 prior 池一致(cdjh 0514 验证 ≥0.02 保留 ~54%)
         rows = rows.stream()
                 .filter(r -> r.getRov() != null && r.getRov() >= DEMAND_MIN_ROV)
@@ -1376,6 +1508,7 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
             if (Objects.isNull(item.getRecommendScore())) {
                 item.setRecommendScore(recommendTypeVideoScoreMap.get(video.getVideoId()));
             }
+            item.setExperimentId("hot");
             result.add(item);
         }
         return result;

+ 122 - 0
api-module/src/main/resources/mapper/contentplatform/ext/ContentPlatformPlanMapperExt.xml

@@ -108,6 +108,12 @@
         <if test="param.category!= null and param.category!= ''">
             and category = #{param.category}
         </if>
+        <if test="param.excludeVideoIds != null and param.excludeVideoIds.size() > 0">
+            and video_id not in
+            <foreach collection="param.excludeVideoIds" item="item" open="(" close=")" separator=",">
+                #{item}
+            </foreach>
+        </if>
     </select>
 
     <select id="getVideoList" resultType="com.tzld.piaoquan.api.model.po.contentplatform.ContentPlatformVideo">
@@ -123,6 +129,12 @@
         <if test="param.category!= null and param.category!= ''">
             and video.category = #{param.category}
         </if>
+        <if test="param.excludeVideoIds != null and param.excludeVideoIds.size() > 0">
+            and video.video_id not in
+            <foreach collection="param.excludeVideoIds" item="item" open="(" close=")" separator=",">
+                #{item}
+            </foreach>
+        </if>
         order by ${sort}
         limit #{offset}, #{pageSize}
     </select>
@@ -419,4 +431,114 @@
           and cpgp.status = 1
     </select>
 
+    <select id="getSelectedQwVideoCount" resultType="java.lang.Integer">
+        select count(distinct v.video_id)
+        from content_platform_qw_plan_video v
+        join content_platform_qw_plan p on p.id = v.plan_id
+        where p.create_account_id = #{loginAccountId}
+          and p.type = #{planType}
+          and p.status = 1
+          and v.status = 1
+        <if test="title != null and title != ''">
+            and v.title like concat('%', #{title}, '%')
+        </if>
+    </select>
+
+    <select id="getSelectedQwVideoList"
+            resultType="com.tzld.piaoquan.api.model.vo.contentplatform.VideoContentItemVO">
+        select t.videoId, t.title, t.cover, t.video, t.experimentId
+        from (
+            select v.video_id as videoId,
+                   v.title as title,
+                   v.cover as cover,
+                   v.video as video,
+                   v.experiment_id as experimentId,
+                   v.create_timestamp as latestTs,
+                   row_number() over (partition by v.video_id order by v.create_timestamp desc) as rn
+            from content_platform_qw_plan_video v
+            join content_platform_qw_plan p on p.id = v.plan_id
+            where p.create_account_id = #{loginAccountId}
+              and p.type = #{planType}
+              and p.status = 1
+              and v.status = 1
+            <if test="title != null and title != ''">
+                and v.title like concat('%', #{title}, '%')
+            </if>
+        ) t
+        where t.rn = 1
+        order by t.latestTs desc
+        limit #{offset}, #{pageSize}
+    </select>
+
+    <select id="getSelectedGzhVideoCount" resultType="java.lang.Integer">
+        select count(distinct v.video_id)
+        from content_platform_gzh_plan_video v
+        join content_platform_gzh_plan p on p.id = v.plan_id
+        where p.create_account_id = #{loginAccountId}
+          and p.type = #{planType}
+          and p.status = 1
+          and v.status = 1
+        <if test="title != null and title != ''">
+            and (v.title like concat('%', #{title}, '%')
+                 or v.custom_title like concat('%', #{title}, '%'))
+        </if>
+    </select>
+
+    <select id="getSelectedGzhVideoList"
+            resultType="com.tzld.piaoquan.api.model.vo.contentplatform.VideoContentItemVO">
+        select t.videoId, t.title, t.cover, t.video, t.experimentId
+        from (
+            select v.video_id as videoId,
+                   coalesce(nullif(v.custom_title, ''), v.title) as title,
+                   coalesce(nullif(v.custom_cover, ''), v.cover) as cover,
+                   v.video as video,
+                   v.experiment_id as experimentId,
+                   v.create_timestamp as latestTs,
+                   row_number() over (partition by v.video_id order by v.create_timestamp desc) as rn
+            from content_platform_gzh_plan_video v
+            join content_platform_gzh_plan p on p.id = v.plan_id
+            where p.create_account_id = #{loginAccountId}
+              and p.type = #{planType}
+              and p.status = 1
+              and v.status = 1
+            <if test="title != null and title != ''">
+                and (v.title like concat('%', #{title}, '%')
+                     or v.custom_title like concat('%', #{title}, '%'))
+            </if>
+        ) t
+        where t.rn = 1
+        order by t.latestTs desc
+        limit #{offset}, #{pageSize}
+    </select>
+
+    <select id="getUserPublishedQwVideoIds" resultType="java.lang.Long">
+        select t.video_id from (
+            select v.video_id as video_id, max(v.create_timestamp) as latest_ts
+            from content_platform_qw_plan_video v
+            join content_platform_qw_plan p on p.id = v.plan_id
+            where p.create_account_id = #{loginAccountId}
+              and p.type = #{planType}
+              and p.status = 1
+              and v.status = 1
+            group by v.video_id
+        ) t
+        order by t.latest_ts desc
+        limit #{limit}
+    </select>
+
+    <select id="getUserPublishedGzhVideoIds" resultType="java.lang.Long">
+        select t.video_id from (
+            select v.video_id as video_id, max(v.create_timestamp) as latest_ts
+            from content_platform_gzh_plan_video v
+            join content_platform_gzh_plan p on p.id = v.plan_id
+            where p.create_account_id = #{loginAccountId}
+              and p.type = #{planType}
+              and p.status = 1
+              and v.status = 1
+            group by v.video_id
+        ) t
+        order by t.latest_ts desc
+        limit #{limit}
+    </select>
+
 </mapper>

+ 20 - 17
common-module/src/main/java/com/tzld/piaoquan/growth/common/service/Impl/MessageAttachmentServiceImpl.java

@@ -37,6 +37,7 @@ import java.io.InputStream;
 import java.io.OutputStream;
 import java.net.HttpURLConnection;
 import java.net.URL;
+import java.net.URLEncoder;
 import java.nio.file.Files;
 import java.nio.file.Paths;
 import java.util.*;
@@ -433,22 +434,23 @@ public class MessageAttachmentServiceImpl implements MessageAttachmentService {
         param.setTestId(experimentId);
         Map<String, String> requestParam = new HashMap<>();
         String experimentIdParam = (experimentId != null && !experimentId.isEmpty()) ? "&experimentId=" + experimentId : "";
-        String growthExtDataParam = "";
-        if (experimentId != null && !experimentId.isEmpty()) {
-            JSONObject growthExtData = new JSONObject();
-            growthExtData.put("experimentId", experimentId);
-            growthExtDataParam = "&growthExtData=" + growthExtData.toJSONString();
-        }
         String jumpPage = "pages/user-videos?fromGzh=1&rootShareId=${uuid}&id=${videoId}&shareId=${uuid}&rootSourceId=[rootSourceId]"
                 .replace("${videoId}", "" + videoId)
                 .replace("${uuid}", "" + UUID.randomUUID())
-                + experimentIdParam + growthExtDataParam;
+                + experimentIdParam;
         requestParam.put("jumpPage", jumpPage);
         param.setRequestParam(requestParam);
         String res = httpPoolClient.post(POST_ADD_TENCENT, JSONObject.toJSONString(param));
         JSONObject jsonObject = JSONObject.parseObject(res);
         JSONObject data = jsonObject.getJSONObject("data");
-        return data.getString("url");
+        String pageUrl = data.getString("url");
+        // 在返回的url字段后拼接growthExtData参数(URL编码,作为jumpPage内部参数)
+        if (experimentId != null && !experimentId.isEmpty() && pageUrl != null) {
+            JSONObject growthExtData = new JSONObject();
+            growthExtData.put("experimentId", experimentId);
+            pageUrl = pageUrl + "%26growthExtData%3D" + URLEncoder.encode(growthExtData.toJSONString(), "UTF-8");
+        }
+        return pageUrl;
     }
 
     public String selectPage(String putScene, String channel, Long videoId, String carrierId, String typeOne, String typeTwo) {
@@ -518,24 +520,25 @@ public class MessageAttachmentServiceImpl implements MessageAttachmentService {
         param.setTestId(experimentId);
         Map<String, String> requestParam = new HashMap<>();
         String experimentIdParam = (experimentId != null && !experimentId.isEmpty()) ? "&experimentId=" + experimentId : "";
-        String growthExtDataParam = "";
-        if (experimentId != null && !experimentId.isEmpty()) {
-            JSONObject growthExtData = new JSONObject();
-            growthExtData.put("experimentId", experimentId);
-            growthExtDataParam = "&growthExtData=" + growthExtData.toJSONString();
-        }
         String jumpPage = "pages/user-videos?fromGzh=1&rootShareId=${uuid}&id=${videoId}&shareId=${uuid}&rootSourceId=[rootSourceId]"
                 .replace("${videoId}", "" + videoId)
                 .replace("${uuid}", "" + UUID.randomUUID())
-                + experimentIdParam + growthExtDataParam;
+                + experimentIdParam;
         requestParam.put("jumpPage", jumpPage);
         param.setRequestParam(requestParam);
         try {
             String res = httpPoolClient.post(POST_ADD_TENCENT, JSONObject.toJSONString(param));
             JSONObject jsonObject = JSONObject.parseObject(res);
             JSONObject data = jsonObject.getJSONObject("data");
-            return data.getString("url");
-        } catch (IOException e) {
+            String pageUrl = data.getString("url");
+            // 在返回的url字段后拼接growthExtData参数(URL编码,作为jumpPage内部参数)
+            if (experimentId != null && !experimentId.isEmpty() && pageUrl != null) {
+                JSONObject growthExtData = new JSONObject();
+                growthExtData.put("experimentId", experimentId);
+                pageUrl = pageUrl + "%26growthExtData%3D" + URLEncoder.encode(growthExtData.toJSONString(), "UTF-8");
+            }
+            return pageUrl;
+        } catch (Exception e) {
             log.error("MessageAttachmentService getPage error", e);
         }
         return null;