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

Merge branch 'master' into test

wangyunpeng 1 неделя назад
Родитель
Сommit
5ab672554b

+ 8 - 0
api-module/src/main/java/com/tzld/piaoquan/api/controller/contentplatform/ContentPlatformDatastatController.java

@@ -136,4 +136,12 @@ public class ContentPlatformDatastatController {
         return CommonResponse.success();
     }
 
+    @ApiOperation(value = "企微合作账号层级类型同步任务", hidden = true)
+    @GetMapping("/job/syncQwHzAccountLayerTypeJob")
+    @JwtIgnore
+    public CommonResponse<String> syncQwHzAccountLayerTypeJob(String dateStr) {
+        job.syncQwHzAccountLayerTypeJob(dateStr);
+        return CommonResponse.success();
+    }
+
 }

+ 4 - 3
api-module/src/main/java/com/tzld/piaoquan/api/dao/mapper/contentplatform/ext/ContentPlatformDemandVideoMapperExt.java

@@ -32,11 +32,12 @@ public interface ContentPlatformDemandVideoMapperExt {
     String getMaxDt(@Param("channelName") String channelName);
 
     /**
-     * 按 ghName(channel_level3) 反查所属 channel_name。
-     * 业务约定一个公众号只归属一个 channel,故 LIMIT 1。
+     * 按 ghName(channel_level3) 反查所属 channel_name,取该 gh 最新分区的一行。
+     * 业务约定一个公众号只归属一个 channel,故 ORDER BY dt DESC LIMIT 1。
+     * (不再先单独查全表 MAX(dt),省掉无 channel 过滤的全表扫描。)
      * 未命中返回 null,由调用方决定回退。
      */
-    String selectChannelNameByGh(@Param("dt") String dt, @Param("ghName") String ghName);
+    String selectChannelNameByGh(@Param("ghName") String ghName);
 
     /**
      * 推荐场景候选池查询:按 demand_strategy 取指定 crowd_segment 的候选行,

+ 122 - 123
api-module/src/main/java/com/tzld/piaoquan/api/job/contentplatform/ContentPlatformDatastatJob.java

@@ -13,8 +13,8 @@ import com.tzld.piaoquan.api.model.po.contentplatform.*;
 import com.tzld.piaoquan.api.model.vo.WxAccountDatastatVO;
 import com.tzld.piaoquan.api.service.contentplatform.ContentPlatformPlanService;
 import com.tzld.piaoquan.growth.common.utils.DateUtil;
-import com.tzld.piaoquan.growth.common.utils.page.Page;
 import com.tzld.piaoquan.growth.common.utils.OdpsUtil;
+import com.tzld.piaoquan.growth.common.utils.RedisUtils;
 import com.xxl.job.core.biz.model.ReturnT;
 import com.xxl.job.core.handler.annotation.XxlJob;
 import lombok.extern.slf4j.Slf4j;
@@ -59,6 +59,9 @@ public class ContentPlatformDatastatJob {
     @Autowired
     private ContentPlatformPlanService planService;
 
+    @Autowired
+    private RedisUtils redisUtils;
+
     @ApolloJsonValue("${unit.price.min:{\"gzh_autoReply\":0.1,\"fwh_push\":0.1,\"qw\":0.15}}")
     private Map<String, Double> unitPriceMinConfig;
 
@@ -639,134 +642,129 @@ public class ContentPlatformDatastatJob {
             dt = param;
         }
         Long now = System.currentTimeMillis();
-        int qwPlanPageSize = 500;
-        int qwPlanPageNum = 1;
-        // 跨批次记录已处理的 rootSourceId,避免重复入库
-        Set<String> existRootSourceIds = new HashSet<>();
-        boolean firstBatch = true;
+
+        // 1. 一次性查询两张 ODPS 表,收集 rootSourceId → firstLevelCount
+        // qw_out 优先,同一个 rootSourceId 在 qw_out 中存在则跳过 qw_out2
+        Map<String, Integer> outFirstLevelMap = new HashMap<>();
+        Map<String, Integer> out2FirstLevelMap = new HashMap<>();
+        int odpsPageSize = 5000;
+
+        int odpsPageNum = 1;
         while (true) {
-            // 分批查询 qwPlan
-            List<ContentPlatformQwPlan> qwPlanPage = getQwPlanByPage(qwPlanPageNum, qwPlanPageSize);
-            if (CollectionUtils.isEmpty(qwPlanPage)) {
+            Integer offset = (odpsPageNum - 1) * odpsPageSize;
+            String outSql = String.format("SELECT rootsourceid, 首层访问人数 " +
+                    "FROM loghubods.qw_out_touliu_behavior_detail " +
+                    "WHERE dt=%s and 首层访问人数 > 0 limit %s,%s;", dt, offset, odpsPageSize);
+            List<Record> outDataList = OdpsUtil.getOdpsData(outSql);
+            if (CollectionUtils.isEmpty(outDataList)) {
+                break;
+            }
+            for (Record record : outDataList) {
+                outFirstLevelMap.put((String) record.get(0), Integer.parseInt((String) record.get(1)));
+            }
+            if (outDataList.size() < odpsPageSize) {
+                break;
+            }
+            odpsPageNum++;
+        }
+
+        odpsPageNum = 1;
+        while (true) {
+            Integer offset = (odpsPageNum - 1) * odpsPageSize;
+            String out2Sql = String.format("SELECT rootsourceid, 首层访问人数 " +
+                    "FROM loghubods.qw_out2_touliu_behavior_detail " +
+                    "WHERE dt=%s and 首层访问人数 > 0 limit %s,%s;", dt, offset, odpsPageSize);
+            List<Record> outDataList = OdpsUtil.getOdpsData(out2Sql);
+            if (CollectionUtils.isEmpty(outDataList)) {
+                break;
+            }
+            for (Record record : outDataList) {
+                String rootSourceId = (String) record.get(0);
+                // qw_out 优先,已存在的跳过
+                if (!outFirstLevelMap.containsKey(rootSourceId)) {
+                    out2FirstLevelMap.put(rootSourceId, Integer.parseInt((String) record.get(1)));
+                }
+            }
+            if (outDataList.size() < odpsPageSize) {
                 break;
             }
-            // 构建当前批次的 map
+            odpsPageNum++;
+        }
+
+        if (outFirstLevelMap.isEmpty() && out2FirstLevelMap.isEmpty()) {
+            return ReturnT.SUCCESS;
+        }
+
+        // 2. 根据 ODPS 查到的 rootSourceId,只查询匹配的 qwPlan(不再查询全量 qwPlan)
+        Set<String> allRootSourceIds = new HashSet<>();
+        allRootSourceIds.addAll(outFirstLevelMap.keySet());
+        allRootSourceIds.addAll(out2FirstLevelMap.keySet());
+        List<String> rootSourceIdList = new ArrayList<>(allRootSourceIds);
+        List<List<String>> rootSourceIdBatches = Lists.partition(rootSourceIdList, 5000);
+
+        boolean firstBatch = true;
+        for (List<String> batchRootSourceIds : rootSourceIdBatches) {
+            List<ContentPlatformQwPlan> qwPlanList = planService.getQwPlanListByRootSourceIds(batchRootSourceIds);
+            if (CollectionUtils.isEmpty(qwPlanList)) {
+                continue;
+            }
+            // 构建 plan 相关 map
             Map<Long, ContentPlatformQwPlan> planMap = new HashMap<>();
-            Map<String, Long> rootSourceIdMap = new HashMap<>();
-            for (ContentPlatformQwPlan plan : qwPlanPage) {
+            Map<String, Long> rootSourceIdToPlanId = new HashMap<>();
+            for (ContentPlatformQwPlan plan : qwPlanList) {
                 planMap.put(plan.getId(), plan);
-                rootSourceIdMap.put(plan.getRootSourceId(), plan.getId());
+                rootSourceIdToPlanId.put(plan.getRootSourceId(), plan.getId());
             }
-            List<String> rootSourceIds = new ArrayList<>(rootSourceIdMap.keySet());
-            // 查询当前批次的 planVideo
+            // 查询 planVideo
             List<Long> planIds = new ArrayList<>(planMap.keySet());
             List<ContentPlatformQwPlanVideo> planVideoList = planService.getQwPlanVideoList(planIds);
             Map<Long, Long> planVideoMap = planVideoList.stream()
                     .collect(Collectors.toMap(ContentPlatformQwPlanVideo::getPlanId, ContentPlatformQwPlanVideo::getVideoId));
-            // 查询当前批次的 videoScore
+            // 查询 videoScore
             List<Long> videoIds = planVideoList.stream().map(ContentPlatformQwPlanVideo::getVideoId).collect(Collectors.toList());
             Map<Long, Double> videoScoreMap = new HashMap<>();
             if (CollectionUtils.isNotEmpty(videoIds)) {
                 List<List<Long>> videoIdPartitions = Lists.partition(videoIds, 2000);
-                List<ContentPlatformVideoAgg> videoList = new ArrayList<>();
                 for (List<Long> partition : videoIdPartitions) {
-                    videoList.addAll(planService.getVideoContentAggListByVideoIds(partition));
+                    List<ContentPlatformVideoAgg> videoList = planService.getVideoContentAggListByVideoIds(partition);
+                    for (ContentPlatformVideoAgg video : videoList) {
+                        videoScoreMap.putIfAbsent(video.getVideoId(), video.getScore());
+                    }
                 }
-                videoScoreMap = videoList.stream()
-                        .collect(Collectors.toMap(ContentPlatformVideoAgg::getVideoId, ContentPlatformVideoAgg::getScore, (a, b) -> a));
             }
-            // 对两张 ODPS 表分页查询,匹配当前批次数据
+            // 遍历当前批次的 rootSourceId,从 ODPS map 中匹配数据
             List<ContentPlatformQwDataStat> saveList = new ArrayList<>();
-            int odpsPageSize = 5000;
-            int odpsPageNum = 1;
-            while (true) {
-                Integer offset = (odpsPageNum - 1) * odpsPageSize;
-                String outSql = String.format("SELECT rootsourceid, 首层访问人数 " +
-                        "FROM loghubods.qw_out_touliu_behavior_detail " +
-                        "WHERE dt=%s and 首层访问人数 > 0 limit %s,%s;", dt, offset, odpsPageSize);
-                List<Record> outDataList = OdpsUtil.getOdpsData(outSql);
-                if (CollectionUtils.isEmpty(outDataList)) {
-                    break;
-                }
-                for (Record record : outDataList) {
-                    String rootSourceId = (String) record.get(0);
-                    if (!rootSourceIds.contains(rootSourceId)) {
-                        continue;
-                    }
-                    if (existRootSourceIds.contains(rootSourceId)) {
-                        continue;
-                    }
-                    int firstLevelCount = Integer.parseInt((String) record.get(1));
-                    if (firstLevelCount == 0) {
-                        continue;
-                    }
-                    ContentPlatformQwDataStat item = new ContentPlatformQwDataStat();
-                    item.setDateStr(dt);
-                    Long planId = rootSourceIdMap.get(rootSourceId);
-                    ContentPlatformQwPlan qwPlan = planMap.get(planId);
-                    item.setSubChannel(StringUtils.hasText(qwPlan.getSubChannel()) ? qwPlan.getSubChannel() : "未知");
-                    Long videoId = planVideoMap.get(planId);
-                    Double score = videoScoreMap.get(videoId);
-                    if (Objects.nonNull(score)) {
-                        BigDecimal num = BigDecimal.valueOf(score);
-                        BigDecimal rounded = num.setScale(2, RoundingMode.HALF_UP);
-                        item.setScore(rounded.doubleValue());
-                    }
-                    item.setRootSourceId(rootSourceId);
-                    item.setFirstLevelCount(firstLevelCount);
-                    item.setCreateTimestamp(now);
-                    saveList.add(item);
-                    existRootSourceIds.add(rootSourceId);
+            for (String rootSourceId : batchRootSourceIds) {
+                Long planId = rootSourceIdToPlanId.get(rootSourceId);
+                if (planId == null) {
+                    continue;
                 }
-                if (outDataList.size() < odpsPageSize) {
-                    break;
+                // qw_out 优先,其次 qw_out2
+                Integer firstLevelCount = outFirstLevelMap.get(rootSourceId);
+                boolean isFromOut = (firstLevelCount != null);
+                if (!isFromOut) {
+                    firstLevelCount = out2FirstLevelMap.get(rootSourceId);
                 }
-                odpsPageNum++;
-            }
-            odpsPageNum = 1;
-            while (true) {
-                Integer offset = (odpsPageNum - 1) * odpsPageSize;
-                String out2Sql = String.format("SELECT rootsourceid, 首层访问人数 " +
-                        "FROM loghubods.qw_out2_touliu_behavior_detail " +
-                        "WHERE dt=%s and 首层访问人数 > 0 limit %s,%s;", dt, offset, odpsPageSize);
-                List<Record> outDataList = OdpsUtil.getOdpsData(out2Sql);
-                if (CollectionUtils.isEmpty(outDataList)) {
-                    break;
+                if (firstLevelCount == null) {
+                    continue;
                 }
-                for (Record record : outDataList) {
-                    String rootSourceId = (String) record.get(0);
-                    if (!rootSourceIds.contains(rootSourceId)) {
-                        continue;
-                    }
-                    if (existRootSourceIds.contains(rootSourceId)) {
-                        continue;
-                    }
-                    int firstLevelCount = Integer.parseInt((String) record.get(1));
-                    if (firstLevelCount == 0) {
-                        continue;
-                    }
-                    ContentPlatformQwDataStat item = new ContentPlatformQwDataStat();
-                    item.setDateStr(dt);
-                    Long planId = rootSourceIdMap.get(rootSourceId);
-                    Long videoId = planVideoMap.get(planId);
-                    Double score = videoScoreMap.get(videoId);
-                    if (Objects.nonNull(score)) {
-                        BigDecimal num = BigDecimal.valueOf(score);
-                        BigDecimal rounded = num.setScale(2, RoundingMode.HALF_UP);
-                        item.setScore(rounded.doubleValue());
-                    }
-                    item.setRootSourceId(rootSourceId);
-                    item.setFirstLevelCount(firstLevelCount);
-                    item.setCreateTimestamp(now);
-                    saveList.add(item);
-                    existRootSourceIds.add(rootSourceId);
+                ContentPlatformQwDataStat item = new ContentPlatformQwDataStat();
+                item.setDateStr(dt);
+                item.setRootSourceId(rootSourceId);
+                item.setFirstLevelCount(firstLevelCount);
+                if (isFromOut) {
+                    ContentPlatformQwPlan qwPlan = planMap.get(planId);
+                    item.setSubChannel(StringUtils.hasText(qwPlan.getSubChannel()) ? qwPlan.getSubChannel() : "未知");
                 }
-                if (outDataList.size() < odpsPageSize) {
-                    break;
+                Long videoId = planVideoMap.get(planId);
+                Double score = videoScoreMap.get(videoId);
+                if (Objects.nonNull(score)) {
+                    item.setScore(BigDecimal.valueOf(score).setScale(2, RoundingMode.HALF_UP).doubleValue());
                 }
-                odpsPageNum++;
+                item.setCreateTimestamp(now);
+                saveList.add(item);
             }
-            // 当前批次处理完后立即入库,第一批先删再插,后续批次直接追加
+            // 入库:第一批先删再插,后续批次直接追加
             if (CollectionUtils.isNotEmpty(saveList)) {
                 if (firstBatch) {
                     dataStatMapperExt.deleteQwDatastat(dt);
@@ -774,28 +772,10 @@ public class ContentPlatformDatastatJob {
                 }
                 dataStatMapperExt.batchInsertQwDatastat(saveList);
             }
-            // 清除当前批次数据,释放内存
-            planMap.clear();
-            rootSourceIdMap.clear();
-            planVideoList.clear();
-            planVideoMap.clear();
-            videoScoreMap.clear();
-            saveList.clear();
-            if (qwPlanPage.size() < qwPlanPageSize) {
-                break;
-            }
-            qwPlanPageNum++;
         }
         return ReturnT.SUCCESS;
     }
 
-    private List<ContentPlatformQwPlan> getQwPlanByPage(int pageNum, int pageSize) {
-        ContentPlatformQwPlanExample example = new ContentPlatformQwPlanExample();
-        Page page = new Page(pageNum, pageSize);
-        example.setPage(page);
-        return qwPlanMapper.selectByExample(example);
-    }
-
     @XxlJob("syncContentPlatformQwDatastatTotalJob")
     public ReturnT<String> syncContentPlatformQwDatastatTotalJob(String param) {
         String dt = DateUtil.getBeforeDayDateString("yyyyMMdd");
@@ -1128,4 +1108,23 @@ public class ContentPlatformDatastatJob {
         }
         return null;
     }
+
+    @XxlJob("syncQwHzAccountLayerTypeJob")
+    public ReturnT<String> syncQwHzAccountLayerTypeJob(String param) {
+        String sql = "SELECT partner_channel, layer_type FROM loghubods.feishu_wechat_qw_hz_account_base;";
+        List<Record> dataList = OdpsUtil.getOdpsData(sql);
+        if (CollectionUtils.isEmpty(dataList)) {
+            log.info("syncQwHzAccountLayerTypeJob no data found");
+            return ReturnT.SUCCESS;
+        }
+        for (Record record : dataList) {
+            String partnerChannel = (String) record.get(0);
+            String layerType = (String) record.get(1);
+            if (StringUtils.hasText(partnerChannel) && StringUtils.hasText(layerType)) {
+                redisUtils.set("cp:qw_hz_layer_type:" + partnerChannel, layerType, RedisUtils.DEFAULT_EXPIRE_TIME);
+            }
+        }
+        log.info("syncQwHzAccountLayerTypeJob done, size: {}", dataList.size());
+        return ReturnT.SUCCESS;
+    }
 }

+ 3 - 0
api-module/src/main/java/com/tzld/piaoquan/api/model/vo/contentplatform/VideoContentItemVO.java

@@ -81,6 +81,9 @@ public class VideoContentItemVO {
     @ApiModelProperty(value = "标准元素")
     private String standardElement;
 
+    @ApiModelProperty(value = "品类")
+    private String category = "";
+
     @ApiModelProperty(value = "分类名称")
     private String categoryName;
 

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

@@ -642,6 +642,8 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
         return Arrays.asList(
                 // 策略1: 人群需求*场景已看
                 new PriorPoolConfig(PriorDimensionEnum.PREMIUM, DemandTypeEnum.STANDARD, DemandMatchMethodEnum.SCENE, true),
+                new PriorPoolConfig(PriorDimensionEnum.GROWTH, DemandTypeEnum.STANDARD, DemandMatchMethodEnum.SCENE, true),
+                new PriorPoolConfig(PriorDimensionEnum.DISTRIBUTION, DemandTypeEnum.STANDARD, DemandMatchMethodEnum.SCENE, true),
                 // 策略2: 人群需求*特征点*向量匹配
                 new PriorPoolConfig(PriorDimensionEnum.PREMIUM, DemandTypeEnum.STANDARD, DemandMatchMethodEnum.VECTOR_SIMILARITY, false),
                 new PriorPoolConfig(PriorDimensionEnum.GROWTH, DemandTypeEnum.STANDARD, DemandMatchMethodEnum.VECTOR_SIMILARITY, false),
@@ -692,10 +694,7 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
         }
         String ghName = param.getGhName();
         if (StringUtils.hasText(ghName)) {
-            String dt = demandVideoMapperExt.getMaxDt(null);
-            if (StringUtils.hasText(dt)) {
-                return DemandChannelEnum.fromValue(demandVideoMapperExt.selectChannelNameByGh(dt, ghName));
-            }
+            return DemandChannelEnum.fromValue(demandVideoMapperExt.selectChannelNameByGh(ghName));
         }
         return null;
     }
@@ -768,6 +767,9 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
         if (VideoContentSource.HOT == source) {
             return getHotSourcePaged(param, user);
         }
+        // 杠杆1: channel + dt 每请求只解析一次,传入各池,避免 fan-out 各池重复查 channel/dt
+        DemandChannelEnum channel = resolveChannelName(param);
+        String dt = demandVideoMapperExt.getMaxDt(channel == null ? null : channel.getValue());
         List<VideoContentItemVO> list;
         if (VideoContentSource.PRIOR == source) {
             // 人群需求池并行拉取, 按 PRIOR_POOL_CONFIGS 配置
@@ -777,8 +779,8 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
                 List<Future<List<VideoContentItemVO>>> futures = new ArrayList<>(poolCount);
                 for (PriorPoolConfig cfg : PRIOR_POOL_CONFIGS) {
                     futures.add(executor.submit(cfg.scene
-                            ? () -> fetchPriorSceneCandidates(param, user, DEMAND_CANDIDATE_LIMIT, cfg.dimension, cfg.demandType, cfg.matchMethod)
-                            : () -> fetchPriorPool(param, user, DEMAND_CANDIDATE_LIMIT, cfg.dimension, cfg.demandType, cfg.matchMethod)));
+                            ? () -> fetchPriorSceneCandidates(param, user, DEMAND_CANDIDATE_LIMIT, channel, dt, cfg.dimension, cfg.demandType, cfg.matchMethod)
+                            : () -> fetchPriorPool(param, user, DEMAND_CANDIDATE_LIMIT, channel, dt, cfg.dimension, cfg.demandType, cfg.matchMethod)));
                 }
 
                 int timeoutSeconds = 30;
@@ -795,7 +797,7 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
                 executor.shutdown();
             }
         } else {
-            list = fetchPosteriorCandidates(param, user, DEMAND_CANDIDATE_LIMIT);
+            list = fetchPosteriorCandidates(param, user, DEMAND_CANDIDATE_LIMIT, channel, dt);
         }
         for (VideoContentItemVO v : list) {
             v.setSource(source.getValue());
@@ -805,21 +807,15 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
 
     /**
      * 通用 N 池轮转穿插:
-     * - 场景已看视频池(索引0)固定首位,其余池随机打乱后按序轮转,每池取 1 条
-     * - TODO: 场景已看池扩充多 dimension 后去掉首位固定,全部随机打乱
+     * - 池列表随机打乱后,按新顺序轮转,每池取 1 条,跳过已耗尽的池继续下一轮
      * - 跨池 video_id / 标题去重;某池耗尽后自动从轮转中移除
      */
     private List<VideoContentItemVO> interleaveMultiPools(List<List<VideoContentItemVO>> pools) {
         int n = pools.size();
-        // 池顺序: 首位固定(场景已看视频池), 其余随机打乱
+        // 随机打乱池顺序,轮转遍历
         List<Integer> order = new ArrayList<>(n);
-        if (n > 0) order.add(0); // 场景已看视频池固定首位
-        if (n > 1) {
-            List<Integer> tail = new ArrayList<>(n - 1);
-            for (int i = 1; i < n; i++) tail.add(i);
-            Collections.shuffle(tail);
-            order.addAll(tail);
-        }
+        for (int i = 0; i < n; i++) order.add(i);
+        Collections.shuffle(order);
 
         int[] pointers = new int[n];
         boolean[] exhausted = new boolean[n];
@@ -927,18 +923,21 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
     private Page<VideoContentItemVO> getInterleavedPage(VideoContentListParam param, ContentPlatformAccount user) {
         int priorCount = PRIOR_POOL_CONFIGS.size();
         int totalCount = priorCount + 2; // + posterior + hot
+        // 杠杆1: channel + dt 每请求只解析一次,传入各 demand 池(hot 池用自己的 dt,不受影响)
+        DemandChannelEnum channel = resolveChannelName(param);
+        String dt = demandVideoMapperExt.getMaxDt(channel == null ? null : channel.getValue());
         ExecutorService executor = Executors.newFixedThreadPool(totalCount);
         try {
             // prior 池并行拉取
             List<Future<List<VideoContentItemVO>>> futures = new ArrayList<>(totalCount);
             for (PriorPoolConfig cfg : PRIOR_POOL_CONFIGS) {
                 futures.add(executor.submit(cfg.scene
-                        ? () -> fetchPriorSceneCandidates(param, user, DEMAND_CANDIDATE_LIMIT, cfg.dimension, cfg.demandType, cfg.matchMethod)
-                        : () -> fetchPriorPool(param, user, DEMAND_CANDIDATE_LIMIT, cfg.dimension, cfg.demandType, cfg.matchMethod)));
+                        ? () -> fetchPriorSceneCandidates(param, user, DEMAND_CANDIDATE_LIMIT, channel, dt, cfg.dimension, cfg.demandType, cfg.matchMethod)
+                        : () -> fetchPriorPool(param, user, DEMAND_CANDIDATE_LIMIT, channel, dt, cfg.dimension, cfg.demandType, cfg.matchMethod)));
             }
             // posterior + hot
             Future<List<VideoContentItemVO>> fPosterior = executor.submit(
-                    () -> fetchPosteriorCandidates(param, user, DEMAND_CANDIDATE_LIMIT));
+                    () -> fetchPosteriorCandidates(param, user, DEMAND_CANDIDATE_LIMIT, channel, dt));
             Future<List<VideoContentItemVO>> fHot = executor.submit(
                     () -> fetchHotCandidates(param, user, HOT_CANDIDATE_LIMIT));
             futures.add(fPosterior);
@@ -987,9 +986,8 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
      * 3. 输出顺序按 sceneSumRov DESC,相同再按 total_rov DESC 兜底
      */
     private List<VideoContentItemVO> fetchPriorSceneCandidates(VideoContentListParam param, ContentPlatformAccount user, int limit,
+                                                               DemandChannelEnum channel, String dt,
                                                                PriorDimensionEnum dimension, DemandTypeEnum demandType, DemandMatchMethodEnum matchMethod) {
-        DemandChannelEnum channel = resolveChannelName(param);
-        String dt = demandVideoMapperExt.getMaxDt(channel == null ? null : channel.getValue());
         if (!StringUtils.hasText(dt)) {
             return new ArrayList<>();
         }
@@ -1050,9 +1048,8 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
      * 6. 截断到 limit
      */
     private List<VideoContentItemVO> fetchPriorPool(VideoContentListParam param, ContentPlatformAccount user, int limit,
+                                                    DemandChannelEnum channel, String dt,
                                                     PriorDimensionEnum dimension, DemandTypeEnum demandType, DemandMatchMethodEnum matchMethod) {
-        DemandChannelEnum channel = resolveChannelName(param);
-        String dt = demandVideoMapperExt.getMaxDt(channel == null ? null : channel.getValue());
         if (!StringUtils.hasText(dt)) {
             return new ArrayList<>();
         }
@@ -1135,9 +1132,8 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
      * 按 demand_content_id 分组,组按 total_rov DESC、组内 score DESC 取前 K;
      * 跨组用 video_id + 归一化标题去重,截到 limit。
      */
-    private List<VideoContentItemVO> fetchPosteriorCandidates(VideoContentListParam param, ContentPlatformAccount user, int limit) {
-        DemandChannelEnum channel = resolveChannelName(param);
-        String dt = demandVideoMapperExt.getMaxDt(channel == null ? null : channel.getValue());
+    private List<VideoContentItemVO> fetchPosteriorCandidates(VideoContentListParam param, ContentPlatformAccount user, int limit,
+                                                              DemandChannelEnum channel, String dt) {
         if (!StringUtils.hasText(dt)) {
             return new ArrayList<>();
         }
@@ -1502,6 +1498,7 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
             if (Objects.isNull(item.getRecommendScore())) {
                 item.setRecommendScore(recommendTypeVideoScoreMap.get(video.getVideoId()));
             }
+            item.setCategory(defaultString(video.getCategory()));
             item.setExperimentId("hot");
             result.add(item);
         }
@@ -1730,6 +1727,11 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
             String pageUrl = messageAttachmentService.getPage(loginUser.getChannel(), carrierId,
                     "dyyqw", "企微", QwPlanTypeEnum.from(param.getType()).getDescription(),
                     "位置1", videoParam.getVideoId(), videoParam.getExperimentId());
+            // 判断是否为特殊首层,追加 isSpecialLayer 参数
+            String layerType = redisUtils.getString("cp:qw_hz_layer_type:" + loginUser.getChannel());
+            if ("特殊".equals(layerType)) {
+                pageUrl = pageUrl + "%26isSpecialLayer%3D1";
+            }
             //pageUrl = videoMultiService.setVideoMultiTitleCoverPagePath(videoParam.getVideoId(), pageUrl,
             //        videoParam.getTitle(), videoParam.getCover());
             String rootSourceId = MessageUtil.getRootSourceId(pageUrl);
@@ -2104,6 +2106,7 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
             item.setDemandContentTopic(video.getDemandContentTopic());
             item.setPointType(video.getPointType());
             item.setStandardElement(video.getStandardElement());
+            item.setCategory(defaultString(video.getCategory()));
             item.setCategoryName(video.getCategoryName());
             item.setExperimentId(video.getExperimentId());
             item.setSim(video.getSim());
@@ -2121,6 +2124,10 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
         return result;
     }
 
+    private static String defaultString(String value) {
+        return StringUtils.hasText(value) ? value : "";
+    }
+
     @Override
     public Page<XcxPlanItemVO> xcxPlanList(XcxPlanListParam param) {
         ContentPlatformAccount loginAccount = LoginUserContext.getUser();

+ 7 - 6
api-module/src/main/resources/mapper/contentplatform/ext/ContentPlatformDemandVideoMapperExt.xml

@@ -91,18 +91,19 @@
     <select id="getMaxDt" resultType="java.lang.String">
         SELECT MAX(dt)
         FROM content_platform_demand_video
-        WHERE status = 1
-        <if test="channelName != null and channelName != ''">
-            AND channel_name = #{channelName}
-        </if>
+        <where>
+            <if test="channelName != null and channelName != ''">
+                channel_name = #{channelName}
+            </if>
+        </where>
     </select>
 
     <select id="selectChannelNameByGh" resultType="java.lang.String">
         SELECT channel_name
         FROM content_platform_demand_video
-        WHERE dt = #{dt}
-          AND channel_level3 = #{ghName}
+        WHERE channel_level3 = #{ghName}
           AND status = 1
+        ORDER BY dt DESC
         LIMIT 1
     </select>