Jelajahi Sumber

Merge branch '20260703-wyp-demandRecommend' of Server/long-article-recommend into master

wangyunpeng 7 jam lalu
induk
melakukan
8dd5d5a035
49 mengubah file dengan 2534 tambahan dan 52 penghapusan
  1. 24 0
      long-article-recommend-service/pom.xml
  2. 4 0
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/Application.java
  3. 1 0
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/common/constant/SceneConstants.java
  4. 3 0
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/common/enums/recommend/ContentPoolEnum.java
  5. 2 0
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/common/enums/recommend/RankStrategyEnum.java
  6. 2 0
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/mapper/aigc/AigcBaseMapper.java
  7. 33 1
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/mapper/longArticle/LongArticleBaseMapper.java
  8. 1 0
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/model/dto/Content.java
  9. 21 0
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/model/dto/DemandSearchArticleRelationDTO.java
  10. 39 0
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/model/entity/longArticle/AccountDemandUtilizePlan.java
  11. 45 0
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/model/entity/longArticle/DemandPoolPromotionBind.java
  12. 62 0
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/model/entity/longArticle/DemandPoolPromotionSource.java
  13. 45 0
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/model/entity/longArticle/DemandProduceContentRelation.java
  14. 30 0
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/model/param/PlanColdStartSortRequest.java
  15. 1 0
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/model/param/RecommendRequest.java
  16. 66 0
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/model/vo/PlanColdStartSortResponse.java
  17. 136 0
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/remote/aigc/AIGCProducePlanCreateService.java
  18. 2 0
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/repository/aigc/PublishAccountRepository.java
  19. 14 0
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/repository/longArticle/AccountDemandUtilizePlanRepository.java
  20. 18 0
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/repository/longArticle/DemandPoolPromotionBindRepository.java
  21. 21 0
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/repository/longArticle/DemandPoolPromotionSourceRepository.java
  22. 23 0
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/repository/longArticle/DemandProduceContentRelationRepository.java
  23. 315 6
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/service/XxlJobService.java
  24. 4 2
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/service/recommend/ArticleCategoryService.java
  25. 267 19
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/service/recommend/ArticlePromotionService.java
  26. 64 0
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/service/recommend/ArticleService.java
  27. 2 1
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/service/recommend/ArticleVideoAuditService.java
  28. 2 1
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/service/recommend/DataDashboardService.java
  29. 142 0
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/service/recommend/FilterContentByAccountService.java
  30. 2 1
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/service/recommend/PublishContentFilterService.java
  31. 3 1
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/service/recommend/RankContentScoreService.java
  32. 641 0
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/service/recommend/RecommendService.java
  33. 27 0
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/service/recommend/filter/FilterService.java
  34. 62 0
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/service/recommend/filter/strategy/DemandPoolPromotionStrategy.java
  35. 4 0
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/service/recommend/rank/RankService.java
  36. 128 0
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/service/recommend/rank/strategy/DemandPoolRankStrategy.java
  37. 9 0
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/service/recommend/score/ScoreService.java
  38. 13 3
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/web/XxlJobController.java
  39. 37 0
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/web/recommend/FilterContentByAccountController.java
  40. 13 0
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/web/recommend/FilterContentByAccountRequest.java
  41. 21 2
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/web/recommend/RecommendController.java
  42. 14 1
      long-article-recommend-service/src/main/resources/application-dev.yml
  43. 9 12
      long-article-recommend-service/src/main/resources/application-pre.yml
  44. 14 1
      long-article-recommend-service/src/main/resources/application-prod.yml
  45. 14 1
      long-article-recommend-service/src/main/resources/application-test.yml
  46. 9 0
      long-article-recommend-service/src/main/resources/application.yml
  47. 26 0
      long-article-recommend-service/src/main/resources/file/长文_生成计划_账号需求利用池.json
  48. 10 0
      long-article-recommend-service/src/main/resources/mapper/aigc/AigcBaseMapper.xml
  49. 89 0
      long-article-recommend-service/src/main/resources/mapper/longArticle/LongArticleBaseMapper.xml

+ 24 - 0
long-article-recommend-service/pom.xml

@@ -147,6 +147,30 @@
             <artifactId>ons-client</artifactId>
             <version>1.8.4.Final</version>
         </dependency>
+
+        <!-- Eureka 客户端 -->
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
+        </dependency>
+
+        <!-- Jersey 客户端(Eureka 依赖) -->
+        <dependency>
+            <groupId>com.sun.jersey</groupId>
+            <artifactId>jersey-client</artifactId>
+            <version>1.19.4</version>
+        </dependency>
+        <dependency>
+            <groupId>com.sun.jersey.contribs</groupId>
+            <artifactId>jersey-apache-client4</artifactId>
+            <version>1.19.4</version>
+        </dependency>
+
+        <!-- OpenFeign -->
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-starter-openfeign</artifactId>
+        </dependency>
     </dependencies>
 
 

+ 4 - 0
long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/Application.java

@@ -6,6 +6,8 @@ import org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration;
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
 import org.springframework.boot.autoconfigure.data.redis.RedisReactiveAutoConfiguration;
+import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
+import org.springframework.cloud.openfeign.EnableFeignClients;
 import org.springframework.context.annotation.ComponentScan;
 import org.springframework.context.annotation.EnableAspectJAutoProxy;
 
@@ -28,6 +30,8 @@ import org.springframework.context.annotation.EnableAspectJAutoProxy;
 })
 @EnableAspectJAutoProxy
 @EnableApolloConfig
+@EnableDiscoveryClient
+@EnableFeignClients
 public class Application {
     public static void main(String[] args) {
         SpringApplication.run(Application.class, args);

+ 1 - 0
long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/common/constant/SceneConstants.java

@@ -6,4 +6,5 @@ package com.tzld.longarticle.recommend.server.common.constant;
 public class SceneConstants {
     public static final String DEFAULT = "DEFAULT";
     public static final String FWH_COLD_START = "FWH_COLD_START";
+    public static final String DEMAND_POOL = "DEMAND_POOL";
 }

+ 3 - 0
long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/common/enums/recommend/ContentPoolEnum.java

@@ -13,6 +13,9 @@ public enum ContentPoolEnum {
     autoArticlePoolLevel3("autoArticlePoolLevel3", 3, "次条", 25),
     autoArticlePoolLevel4("autoArticlePoolLevel4", 4, "冷启层", 1),
 
+    accountDemandPoolLevelCold("accountDemandPoolLevelCold", 5, "需求冷启池", 1),
+    accountDemandPoolLevelUtilize("accountDemandPoolLevelUtilize", 6, "需求利用池", 100),
+
     ;
 
     private final String contentPool;

+ 2 - 0
long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/common/enums/recommend/RankStrategyEnum.java

@@ -34,6 +34,8 @@ public enum RankStrategyEnum {
     RANDOM_STRATEGY("ArticleRankRandom", "随机策略", "randomRankStrategy"),
     DEFAULT_STRATEGY("ArticleRankV1", "默认策略", "defaultRankStrategy"),
     FWH_STRATEGY("FwhStrategy", "服务号策略", "fwhColdStartRankStrategy"),
+
+    DEMAND_POOL_STRATEGY("DemandPoolStrategy", "需求匹配内容池策略", "demandPoolRankStrategy"),
     ;
 
     private final String strategy;

+ 2 - 0
long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/mapper/aigc/AigcBaseMapper.java

@@ -104,4 +104,6 @@ public interface AigcBaseMapper {
     void topContentDeprecate(String topProducePlanId);
 
     List<String> getAllBatchGroupSendAccount();
+
+    List<ProducePlanExeRecord> getProducePlanExeRecordByChannelContentIds(List<String> list);
 }

+ 33 - 1
long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/mapper/longArticle/LongArticleBaseMapper.java

@@ -7,6 +7,7 @@ import com.tzld.longarticle.recommend.server.model.param.ArticleVideoPoolSourceP
 import com.tzld.longarticle.recommend.server.model.vo.CategoryGroupFunnelExport;
 import com.tzld.longarticle.recommend.server.model.vo.DailyAuditProduceExport;
 import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
 
 import java.util.Date;
 import java.util.List;
@@ -87,7 +88,7 @@ public interface LongArticleBaseMapper {
 
     List<String> getMatchContentId(Date start, Date end);
 
-    void batchInsertLongArticleAuditDelete(List<LongArticleAuditDelete> deleteList);
+    void batchInsertLongArticleAuditDelete(List<LongArticleAuditDelete> list);
 
     List<String> getPassContentIds();
 
@@ -131,4 +132,35 @@ public interface LongArticleBaseMapper {
 
     List<DatastatSortStrategy> getByAccountAndOptionalParams(String accountName, String type, Integer position,
                                                              String dateStrStart, String dateStrEnd);
+
+    List<DemandSearchArticleRelationDTO> getDemandSearchArticleRelations();
+
+    void batchInsertIgnoreDemandProduceContentRelation(List<DemandSearchArticleRelationDTO> list);
+
+    List<DemandSearchArticleRelationDTO> getDemandExperimentIdByChannelContentIds(List<String> list);
+
+    List<DemandSearchArticleRelationDTO> getDemandPoolPromotionByLevel(String level, Integer status, Integer deleted);
+
+    List<DemandSearchArticleRelationDTO> getDemandExperimentIdByContentIds(@Param("contentIds") List<String> contentIds,
+                                                                           @Param("accountId") String accountId);
+
+    /**
+     * 按 planId + accountIds + contentPoolType 查询冷启内容
+     */
+    List<PublishContentGzhWaiting> getByPlanIdAndAccountIdsAndContentPoolType(@Param("planId") String planId,
+                                                                               @Param("accountIds") List<String> accountIds,
+                                                                               @Param("contentPoolType") String contentPoolType);
+
+    /**
+     * 批量更新 sourceId 的等待状态
+     */
+    void batchUpdateContentStatusBySourceIds(@Param("sourceIds") List<String> sourceIds,
+                                             @Param("status") Integer status,
+                                             @Param("updateTimestamp") Long updateTimestamp);
+
+    /**
+     * 通过 experiment_id 查询 demand_search_queue 获取账号名称
+     */
+    List<DemandSearchArticleRelationDTO> getAccountByExperimentIds(@Param("list") List<String> experimentIds);
+
 }

+ 1 - 0
long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/model/dto/Content.java

@@ -30,6 +30,7 @@ public class Content {
     private String producePlanName;
     private String contentPoolType; // 内容池类别
     private String crawlerChannelContentId; // 抓取内容channelContentId
+    private String experimentId; // 需求匹配实验ID
     private List<String> category; // 品类
     private String crawlerLink;
     private String crawlerTitle;

+ 21 - 0
long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/model/dto/DemandSearchArticleRelationDTO.java

@@ -0,0 +1,21 @@
+package com.tzld.longarticle.recommend.server.model.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 需求搜索文章关系 DTO
+ * 用于查询 demand_search_article_relation 和写入 demand_produce_content_relation
+ *
+ * @author dyp
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class DemandSearchArticleRelationDTO {
+    private String experimentId;
+    private String channelContentId;
+    private String contentId;
+    private String accountId;
+}

+ 39 - 0
long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/model/entity/longArticle/AccountDemandUtilizePlan.java

@@ -0,0 +1,39 @@
+package com.tzld.longarticle.recommend.server.model.entity.longArticle;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import javax.persistence.*;
+
+/**
+ * 账号 → accountDemandPoolLevelUtilize 生成计划映射
+ *
+ * @author dyp
+ */
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@Entity
+@Table(name = "account_demand_utilize_plan")
+public class AccountDemandUtilizePlan {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private Long id;
+
+    @Column(name = "account_id")
+    private String accountId;
+
+    @Column(name = "produce_plan_id")
+    private String producePlanId;
+
+    @Column(name = "plan_name")
+    private String planName;
+
+    @Column(name = "created_at")
+    private Long createdAt;
+
+    @Column(name = "updated_at")
+    private Long updatedAt;
+}

+ 45 - 0
long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/model/entity/longArticle/DemandPoolPromotionBind.java

@@ -0,0 +1,45 @@
+package com.tzld.longarticle.recommend.server.model.entity.longArticle;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import javax.persistence.*;
+
+/**
+ * 需求池晋级生成计划绑定记录
+ *
+ * @author dyp
+ */
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@Entity
+@Table(name = "demand_pool_promotion_bind")
+public class DemandPoolPromotionBind {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private Long id;
+
+    @Column(name = "promotion_channel_content_id")
+    private String promotionChannelContentId;
+
+    @Column(name = "account_id")
+    private String accountId;
+
+    @Column(name = "root_produce_content_id")
+    private String rootProduceContentId;
+
+    @Column(name = "crawler_plan_id")
+    private String crawlerPlanId;
+
+    @Column(name = "produce_plan_id")
+    private String producePlanId;
+
+    @Column(name = "created_at")
+    private Long createdAt;
+
+    @Column(name = "updated_at")
+    private Long updatedAt;
+}

+ 62 - 0
long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/model/entity/longArticle/DemandPoolPromotionSource.java

@@ -0,0 +1,62 @@
+package com.tzld.longarticle.recommend.server.model.entity.longArticle;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.Table;
+
+/**
+ * 需求池晋级记录表
+ * 记录冷启池 → 利用池的晋级关系,与老晋级表 article_pool_promotion_source 分离
+ *
+ * @author dyp
+ */
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@Entity
+@Table(name = "demand_pool_promotion_source")
+public class DemandPoolPromotionSource {
+
+    @Id
+    @Column(name = "channel_content_id")
+    private String channelContentId;
+
+    @Column(name = "wx_sn")
+    private String wxSn;
+
+    @Column(name = "source_publish_content_id")
+    private String sourcePublishContentId;
+
+    @Column(name = "root_produce_content_id")
+    private String rootProduceContentId;
+
+    @Column(name = "title")
+    private String title;
+
+    @Column(name = "title_md5")
+    private String titleMd5;
+
+    @Column(name = "level")
+    private String level;
+
+    @Column(name = "account_id")
+    private String accountId;
+
+    @Column(name = "status")
+    private Integer status;
+
+    @Column(name = "deleted")
+    private Integer deleted;
+
+    @Column(name = "create_timestamp")
+    private Long createTimestamp;
+
+    @Column(name = "update_timestamp")
+    private Long updateTimestamp;
+
+}

+ 45 - 0
long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/model/entity/longArticle/DemandProduceContentRelation.java

@@ -0,0 +1,45 @@
+package com.tzld.longarticle.recommend.server.model.entity.longArticle;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import javax.persistence.*;
+import java.time.LocalDateTime;
+
+/**
+ * 需求搜索文章-生成内容对照关系表
+ * 记录 experiment_id → channel_content_id → plan_exe_id 的映射关系
+ *
+ * @author dyp
+ */
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@Entity
+@Table(name = "demand_produce_content_relation")
+public class DemandProduceContentRelation {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private Long id;
+
+    @Column(name = "account_id")
+    private String accountId;
+
+    @Column(name = "content_id")
+    private String contentId;
+
+    @Column(name = "channel_content_id")
+    private String channelContentId;
+
+    @Column(name = "experiment_id")
+    private String experimentId;
+
+    @Column(name = "created_at")
+    private LocalDateTime createdAt;
+
+    @Column(name = "updated_at")
+    private LocalDateTime updatedAt;
+
+}

+ 30 - 0
long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/model/param/PlanColdStartSortRequest.java

@@ -0,0 +1,30 @@
+package com.tzld.longarticle.recommend.server.model.param;
+
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import java.util.List;
+
+/**
+ * 跨账号冷启池排序请求
+ *
+ * @author dyp
+ */
+@NoArgsConstructor
+@Getter
+@Setter
+public class PlanColdStartSortRequest {
+    private String planId;
+    private String sortBatchId;
+    private List<AccountInfo> accounts;
+
+    @NoArgsConstructor
+    @Getter
+    @Setter
+    public static class AccountInfo {
+        private String accountId;
+        private String accountName;
+        private String ghId;
+    }
+}

+ 1 - 0
long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/model/param/RecommendRequest.java

@@ -20,6 +20,7 @@ public class RecommendRequest {
     private String strategy;
     private Integer publishNum;
     private String planId;
+    private String planTag;
     private Integer pushType;
     // 排除文章位置
     private List<Integer> excludeContentIndex;

+ 66 - 0
long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/model/vo/PlanColdStartSortResponse.java

@@ -0,0 +1,66 @@
+package com.tzld.longarticle.recommend.server.model.vo;
+
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 跨账号冷启池排序响应
+ *
+ * @author dyp
+ */
+@NoArgsConstructor
+@Getter
+@Setter
+public class PlanColdStartSortResponse {
+    private Integer code;
+    private String msg;
+    private List<AccountAllocation> data;
+    private List<String> allocatedSourceIds = new ArrayList<>();
+
+    public static PlanColdStartSortResponse success(List<AccountAllocation> data, List<String> allocatedSourceIds) {
+        PlanColdStartSortResponse response = new PlanColdStartSortResponse();
+        response.setCode(0);
+        response.setMsg("success");
+        response.setData(data);
+        response.setAllocatedSourceIds(allocatedSourceIds);
+        return response;
+    }
+
+    public static PlanColdStartSortResponse fail(String msg) {
+        PlanColdStartSortResponse response = new PlanColdStartSortResponse();
+        response.setCode(-1);
+        response.setMsg(msg);
+        return response;
+    }
+
+    @NoArgsConstructor
+    @Getter
+    @Setter
+    public static class AccountAllocation {
+        private String accountId;
+        private String accountName;
+        private String ghId;
+        private List<PositionAllocation> positions;
+    }
+
+    @NoArgsConstructor
+    @Getter
+    @Setter
+    public static class PositionAllocation {
+        private Integer positionIndex;
+        private String sourceId;
+        private String publishContentId;
+        private Integer sourceType;
+        private String title;
+        private Double score;
+        private String contentPoolType;
+        private String experimentId;
+        private boolean skipped;
+        private String skipReason;
+        private Double positionAvgRead;
+    }
+}

+ 136 - 0
long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/remote/aigc/AIGCProducePlanCreateService.java

@@ -0,0 +1,136 @@
+package com.tzld.longarticle.recommend.server.remote.aigc;
+
+import cn.hutool.core.io.resource.ResourceUtil;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.tzld.longarticle.recommend.server.common.HttpPoolFactory;
+import com.tzld.longarticle.recommend.server.model.vo.IdNameVO;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.http.HttpEntity;
+import org.apache.http.StatusLine;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.util.EntityUtils;
+import org.springframework.stereotype.Service;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Objects;
+import java.util.UUID;
+
+/**
+ * 创建 accountDemandPoolLevelUtilize 生成计划。
+ * 从模板文件读取请求结构,每次生成新的 UUID 替换 taskId / groupId 等唯一标识。
+ *
+ * @author dyp
+ */
+@Service
+@Slf4j
+public class AIGCProducePlanCreateService {
+
+    private static final String TASK_ID1 = "${TASK1_ID}";
+    private static final String TASK_ID2 = "${TASK2_ID}";
+    private static final String TASK_ID3 = "${TASK3_ID}";
+    private static final String GROUP_ID = "${GROUP_ID}";
+
+    private final CloseableHttpClient client = HttpPoolFactory.aigcPool();
+
+    /**
+     * 创建带抓取计划的 demand 利用池共用生成计划(所有账号共用一个)。
+     * 抓取计划 ID 直接注入到 inputSourceGroups[0].inputSources 中,无需后续再调用 articleAddDependPlan 绑定。
+     *
+     * @param accountName     账号名(仅用于日志,计划名固定为共享)
+     * @param crawlerPlanId   抓取计划 ID
+     * @param crawlerPlanName 抓取计划名称(用于 inputSourceLabel)
+     * @return plan ID + name
+     */
+    public IdNameVO<String> createUtilizeProducePlan(String accountName, String crawlerPlanId, String crawlerPlanName) {
+        // 所有账号共用一个生成计划
+        String planName = "账号需求池_利用池";
+
+        int retryTimes = 2;
+        while (retryTimes > 0) {
+            IdNameVO<String> result = post(planName, crawlerPlanId, crawlerPlanName);
+            if (Objects.nonNull(result) && Objects.nonNull(result.getId())) {
+                return result;
+            }
+            retryTimes--;
+        }
+        return new IdNameVO<>();
+    }
+
+    private IdNameVO<String> post(String planName, String crawlerPlanId, String crawlerPlanName) {
+        // 1. 读模板原始字符串
+        String templateStr = ResourceUtil.readUtf8Str("file/长文_生成计划_账号需求利用池.json");
+
+        // 2. 生成新的 UUID
+        String taskId1 = UUID.randomUUID().toString().replace("-", "");
+        String taskId2 = UUID.randomUUID().toString().replace("-", "");
+        String taskId3 = UUID.randomUUID().toString().replace("-", "");
+        String groupId = UUID.randomUUID().toString().replace("-", "");
+
+        // 3. 替换占位符
+        templateStr = templateStr.replace(TASK_ID1, taskId1);
+        templateStr = templateStr.replace(TASK_ID2, taskId2);
+        templateStr = templateStr.replace(TASK_ID3, taskId3);
+        templateStr = templateStr.replace(GROUP_ID, groupId);
+
+        // 4. 解析并替换 name + 注入 crawler plan 到 inputSources
+        JSONObject template = JSONObject.parseObject(templateStr);
+        String apiUrl = template.getString("api_url");
+        JSONObject data = template.getJSONObject("data");
+        JSONObject params = data.getJSONObject("params");
+        params.put("name", planName);
+
+        // 将抓取计划注入到第一个 inputSourceGroup 的 inputSources 中
+        JSONArray inputSourceGroups = params.getJSONArray("inputSourceGroups");
+        if (inputSourceGroups != null && !inputSourceGroups.isEmpty()) {
+            JSONObject group = inputSourceGroups.getJSONObject(0);
+            String inputSourceLabel = String.format("原始帖子-长文-微信公众号-内容添加计划-%s", crawlerPlanName);
+            JSONObject inputSource = new JSONObject();
+            inputSource.put("contentType", 1);
+            inputSource.put("inputSourceType", 2);   // contentPlan
+            inputSource.put("inputSourceValue", crawlerPlanId);
+            inputSource.put("inputSourceLabel", inputSourceLabel);
+            inputSource.put("inputSourceModal", 3);  // 长文
+            inputSource.put("inputSourceChannel", 5); // 公众号
+            group.put("inputSources", new JSONArray().fluentAdd(inputSource));
+        }
+
+        String requestData = data.toString();
+        try {
+            HttpPost httpPost = new HttpPost(apiUrl);
+            StringEntity stringEntity = new StringEntity(requestData, StandardCharsets.UTF_8);
+            httpPost.setHeader("Content-Type", "application/json;charset=UTF-8");
+            httpPost.setEntity(stringEntity);
+            CloseableHttpResponse response = client.execute(httpPost);
+            StatusLine statusLine = response.getStatusLine();
+            if (statusLine.getStatusCode() == 200) {
+                HttpEntity responseEntity = response.getEntity();
+                if (Objects.nonNull(responseEntity)) {
+                    String responseBody = EntityUtils.toString(responseEntity, "UTF-8");
+                    JSONObject jsonObject = JSONObject.parseObject(responseBody);
+                    if (jsonObject.getInteger("code") == 0) {
+                        IdNameVO<String> result = new IdNameVO<>();
+                        JSONObject responseData = jsonObject.getJSONObject("data");
+                        result.setId(responseData.getString("id"));
+                        result.setName(responseData.getString("name"));
+                        log.info("createUtilizeProducePlan success, planId:{} name:{} crawlerPlanId:{}",
+                                result.getId(), result.getName(), crawlerPlanId);
+                        return result;
+                    } else {
+                        log.warn("createUtilizeProducePlan api error, code:{} msg:{}",
+                                jsonObject.getInteger("code"), jsonObject.getString("msg"));
+                    }
+                }
+            } else {
+                log.warn("createUtilizeProducePlan http status:{}", statusLine.getStatusCode());
+            }
+        } catch (IOException e) {
+            log.error("createUtilizeProducePlan error", e);
+        }
+        return null;
+    }
+}

+ 2 - 0
long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/repository/aigc/PublishAccountRepository.java

@@ -16,4 +16,6 @@ public interface PublishAccountRepository extends JpaRepository<PublishAccount,
     List<PublishAccount> getByIdIn(List<String> publishAccountIds);
 
     PublishAccount getById(String publishAccountId);
+
+    List<PublishAccount> findByNameIn(List<String> names);
 }

+ 14 - 0
long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/repository/longArticle/AccountDemandUtilizePlanRepository.java

@@ -0,0 +1,14 @@
+package com.tzld.longarticle.recommend.server.repository.longArticle;
+
+import com.tzld.longarticle.recommend.server.model.entity.longArticle.AccountDemandUtilizePlan;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+/**
+ * @author dyp
+ */
+@Repository
+public interface AccountDemandUtilizePlanRepository extends JpaRepository<AccountDemandUtilizePlan, Long> {
+
+    AccountDemandUtilizePlan getByAccountId(String accountId);
+}

+ 18 - 0
long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/repository/longArticle/DemandPoolPromotionBindRepository.java

@@ -0,0 +1,18 @@
+package com.tzld.longarticle.recommend.server.repository.longArticle;
+
+import com.tzld.longarticle.recommend.server.model.entity.longArticle.DemandPoolPromotionBind;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+/**
+ * @author dyp
+ */
+@Repository
+public interface DemandPoolPromotionBindRepository extends JpaRepository<DemandPoolPromotionBind, Long> {
+
+    DemandPoolPromotionBind getByPromotionChannelContentIdAndAccountId(String promotionChannelContentId, String accountId);
+
+    List<DemandPoolPromotionBind> findByPromotionChannelContentIdInAndAccountIdIn(List<String> promotionChannelContentIds, List<String> accountIds);
+}

+ 21 - 0
long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/repository/longArticle/DemandPoolPromotionSourceRepository.java

@@ -0,0 +1,21 @@
+package com.tzld.longarticle.recommend.server.repository.longArticle;
+
+import com.tzld.longarticle.recommend.server.model.entity.longArticle.DemandPoolPromotionSource;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+/**
+ * 需求池晋级记录表 Repository
+ *
+ * @author dyp
+ */
+@Repository
+public interface DemandPoolPromotionSourceRepository extends JpaRepository<DemandPoolPromotionSource, String> {
+
+    DemandPoolPromotionSource getByChannelContentId(String channelContentId);
+
+    List<DemandPoolPromotionSource> getByStatusAndDeleted(Integer status, Integer deleted);
+
+}

+ 23 - 0
long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/repository/longArticle/DemandProduceContentRelationRepository.java

@@ -0,0 +1,23 @@
+package com.tzld.longarticle.recommend.server.repository.longArticle;
+
+import com.tzld.longarticle.recommend.server.model.entity.longArticle.DemandProduceContentRelation;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+/**
+ * 需求搜索文章-生成内容对照关系表 Repository
+ *
+ * @author dyp
+ */
+@Repository
+public interface DemandProduceContentRelationRepository extends JpaRepository<DemandProduceContentRelation, Long> {
+
+    List<DemandProduceContentRelation> findByExperimentId(String experimentId);
+
+    List<DemandProduceContentRelation> findByChannelContentId(String channelContentId);
+
+    List<DemandProduceContentRelation> findByContentId(String contentId);
+
+}

+ 315 - 6
long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/service/XxlJobService.java

@@ -14,6 +14,7 @@ import com.tzld.longarticle.recommend.server.common.constant.SceneConstants;
 import com.tzld.longarticle.recommend.server.common.enums.FieshuTableColumnDataTypeEnum;
 import com.tzld.longarticle.recommend.server.common.enums.aigc.ChannelEnum;
 import com.tzld.longarticle.recommend.server.common.enums.aigc.MiniprogramUseTypeEnum;
+import com.tzld.longarticle.recommend.server.common.enums.StatusEnum;
 import com.tzld.longarticle.recommend.server.common.enums.aigc.ProduceContentAuditStatusEnum;
 import com.tzld.longarticle.recommend.server.common.enums.aigc.PushTypeEnum;
 import com.tzld.longarticle.recommend.server.common.enums.longArticle.ArticleVideoAuditStatusEnum;
@@ -39,6 +40,7 @@ import com.tzld.longarticle.recommend.server.remote.aigc.AIGCProduceContentAudit
 import com.tzld.longarticle.recommend.server.remote.aigc.AIGCPublishContentDiscardService;
 import com.tzld.longarticle.recommend.server.remote.aigc.AIGCWaitingPublishContentService;
 import com.tzld.longarticle.recommend.server.repository.aigc.ProducePlanRepository;
+import com.tzld.longarticle.recommend.server.repository.aigc.PublishAccountRepository;
 import com.tzld.longarticle.recommend.server.repository.aigc.PublishPlanRepository;
 import com.tzld.longarticle.recommend.server.repository.aigc.PublishPlanSettingRepository;
 import com.tzld.longarticle.recommend.server.repository.crawler.GetOffVideoCrawlerRepository;
@@ -152,6 +154,9 @@ public class XxlJobService {
     @Autowired
     private AIGCPublishContentDiscardService aigcPublishContentDiscardService;
 
+    @Autowired
+    private PublishAccountRepository publishAccountRepository;
+
     @Autowired
     private RedisUtil redisUtil;
 
@@ -799,12 +804,6 @@ public class XxlJobService {
         return ReturnT.SUCCESS;
     }
 
-    @XxlJob("articlePromotionTraceability")
-    public ReturnT<String> articlePromotionTraceability(String channelContentId) {
-        articleService.articlePromotionTraceability(channelContentId);
-        return ReturnT.SUCCESS;
-    }
-
     @XxlJob("articleCategoryJob")
     public ReturnT<String> articleCategoryJob(String param) {
         articleCategoryService.articleCategory();
@@ -828,6 +827,12 @@ public class XxlJobService {
         return ReturnT.SUCCESS;
     }
 
+    @XxlJob("articlePromotionTraceability")
+    public ReturnT<String> articlePromotionTraceability(String channelContentId) {
+        articleService.articlePromotionTraceability(channelContentId);
+        return ReturnT.SUCCESS;
+    }
+
     @XxlJob("articleVideoPoolPromotion")
     public ReturnT<String> articleVideoPoolPromotion(String param) {
         articlePromotionService.articleVideoPoolPromotion(500, 1.1,
@@ -839,6 +844,25 @@ public class XxlJobService {
         return ReturnT.SUCCESS;
     }
 
+    @XxlJob("demandPoolPromotion")
+    public ReturnT<String> demandPoolPromotion(String param) {
+        articlePromotionService.demandPoolPromotion(500, 1.3,
+                Lists.newArrayList(3, 4, 5, 6, 7, 8));
+        return ReturnT.SUCCESS;
+    }
+
+    @XxlJob("demandPoolPromotionTraceability")
+    public ReturnT<String> demandPoolPromotionTraceability(String channelContentId) {
+        articleService.demandPoolPromotionTraceability(channelContentId);
+        return ReturnT.SUCCESS;
+    }
+
+    @XxlJob("demandPoolPromotionBind")
+    public ReturnT<String> demandPoolPromotionBind(String param) {
+        articlePromotionService.demandPoolPromotionBind();
+        return ReturnT.SUCCESS;
+    }
+
     @XxlJob("refreshArticleHisCache")
     public ReturnT<String> refreshArticleHisCache(String param) {
         // 刷新历史表现缓存
@@ -1344,4 +1368,289 @@ public class XxlJobService {
         return param.getStrategy();
     }
 
+    /**
+     * 同步需求搜索文章-生成内容对照关系
+     * <p>
+     * 从两个来源收集 (experiment_id, channel_content_id, plan_exe_id) 三元组,
+     * 写入 demand_produce_content_relation 表:
+     * <pre>
+     *   来源1: demand_search_article_relation — 搜索阶段直接建立的对照关系
+     *   来源2: demand_pool_promotion_source    — 冷启池晋级到利用池后,反向溯源 experiment_id
+     *
+     *   执行流程:
+     *     ① fetchSearchRelations()                     — 读取搜索阶段对照(含 experiment_id)
+     *     ② fetchPromotionRelations()                  — 读取晋级记录,回填 experiment_id
+     *     ③ fetchPlanExeIdByChannelContentIds()        — 跨库(aigc)查 produce_plan_exe_record
+     *     ④ buildInsertList()                          — 装配三元组,过滤无效记录
+     *     ⑤ batchInsertDemandRelation()                — INSERT IGNORE 批量写入
+     * </pre>
+     *
+     * @param param XXL-Job 参数(未使用)
+     */
+    @XxlJob("syncDemandProduceContentRelation")
+    public ReturnT<String> syncDemandProduceContentRelation(String param) {
+        String lockKey = "DemandProduceContentRelationLock";
+        String requestId = UUID.randomUUID().toString();
+        try {
+            if (!redisUtil.tryAcquireLock(lockKey, requestId, 600L)) {
+                log.info("syncDemandProduceContentRelation lock failed, skip");
+                return ReturnT.SUCCESS;
+            }
+            log.info("syncDemandProduceContentRelation start");
+
+            // ① 从 long_articles 库收集两种来源的原始数据
+            List<DemandSearchArticleRelationDTO> searchRelations = fetchSearchRelations();
+            List<DemandSearchArticleRelationDTO> promotionRelations = fetchPromotionRelations();
+
+            // 合并两个来源
+            List<DemandSearchArticleRelationDTO> allRelations = new ArrayList<>();
+            if (CollectionUtils.isNotEmpty(searchRelations)) {
+                allRelations.addAll(searchRelations);
+            }
+            if (CollectionUtils.isNotEmpty(promotionRelations)) {
+                allRelations.addAll(promotionRelations);
+            }
+            if (CollectionUtils.isEmpty(allRelations)) {
+                log.info("syncDemandProduceContentRelation no data from any source, skip");
+                return ReturnT.SUCCESS;
+            }
+            log.info("syncDemandProduceContentRelation total {} relations (search={}, promotion={})",
+                    allRelations.size(),
+                    searchRelations != null ? searchRelations.size() : 0,
+                    promotionRelations != null ? promotionRelations.size() : 0);
+
+            // ② 跨库(aigc)批量查询 produce_plan_exe_record 获取 plan_exe_id
+            Map<String, String> channelContentIdToPlanExeId = fetchPlanExeIdByChannelContentIds(allRelations);
+
+            // ③ 装配三元组 (content_id=plan_exe_id, channel_content_id, experiment_id)
+            List<DemandSearchArticleRelationDTO> insertList = buildInsertList(allRelations, channelContentIdToPlanExeId);
+
+            // ④ INSERT IGNORE 批量写入 demand_produce_content_relation
+            batchInsertDemandRelation(insertList);
+            log.info("syncDemandProduceContentRelation done, total={}, enriched={}",
+                    allRelations.size(), insertList.size());
+            return ReturnT.SUCCESS;
+        } catch (Exception e) {
+            log.error("syncDemandProduceContentRelation error", e);
+            return ReturnT.FAIL;
+        } finally {
+            redisUtil.releaseLock(lockKey, requestId);
+        }
+    }
+
+    /**
+     * 来源1:查询 demand_search_article_relation(status=2 拉取详情成功 + channel_content_id 非空)
+     * 返回 experiment_id + channel_content_id + account_id 三元组。
+     * <p>
+     * account_id 获取链路:
+     * experiment_id → demand_search_queue.account → publish_account.name → publish_account.id
+     */
+    private List<DemandSearchArticleRelationDTO> fetchSearchRelations() {
+        List<DemandSearchArticleRelationDTO> searchRelations = longArticleBaseMapper.getDemandSearchArticleRelations();
+        log.info("syncDemandProduceContentRelation source1(search) found {} relations",
+                searchRelations != null ? searchRelations.size() : 0);
+
+        if (CollectionUtils.isEmpty(searchRelations)) {
+            return searchRelations;
+        }
+
+        // 提取所有 experiment_id
+        List<String> experimentIds = searchRelations.stream()
+                .map(DemandSearchArticleRelationDTO::getExperimentId)
+                .filter(StringUtils::hasText)
+                .distinct()
+                .collect(Collectors.toList());
+
+        if (CollectionUtils.isEmpty(experimentIds)) {
+            return searchRelations;
+        }
+
+        // 1. experiment_id → account(账号名称),从 demand_search_queue 查询
+        Map<String, String> expIdToAccountName = new HashMap<>();
+        for (List<String> partition : Lists.partition(experimentIds, 500)) {
+            List<DemandSearchArticleRelationDTO> queueResults = longArticleBaseMapper.getAccountByExperimentIds(partition);
+            if (CollectionUtils.isNotEmpty(queueResults)) {
+                for (DemandSearchArticleRelationDTO r : queueResults) {
+                    if (StringUtils.hasText(r.getExperimentId()) && StringUtils.hasText(r.getAccountId())) {
+                        expIdToAccountName.put(r.getExperimentId(), r.getAccountId());
+                    }
+                }
+            }
+        }
+        log.info("syncDemandProduceContentRelation source1 found {} experiment_id -> account_name mappings",
+                expIdToAccountName.size());
+
+        // 2. account名称 → account_id,从 publish_account 查询
+        List<String> accountNames = new ArrayList<>(new HashSet<>(expIdToAccountName.values()));
+        Map<String, String> accountNameToId = new HashMap<>();
+        for (List<String> partition : Lists.partition(accountNames, 500)) {
+            List<PublishAccount> accounts = publishAccountRepository.findByNameIn(partition);
+            if (CollectionUtils.isNotEmpty(accounts)) {
+                for (PublishAccount account : accounts) {
+                    if (StringUtils.hasText(account.getName()) && StringUtils.hasText(account.getId())) {
+                        accountNameToId.put(account.getName(), account.getId());
+                    }
+                }
+            }
+        }
+        log.info("syncDemandProduceContentRelation source1 found {} account_name -> account_id mappings",
+                accountNameToId.size());
+
+        // 3. 回填 accountId
+        for (DemandSearchArticleRelationDTO relation : searchRelations) {
+            String accountName = expIdToAccountName.get(relation.getExperimentId());
+            if (StringUtils.hasText(accountName)) {
+                String accountId = accountNameToId.get(accountName);
+                if (StringUtils.hasText(accountId)) {
+                    relation.setAccountId(accountId);
+                }
+            }
+        }
+
+        return searchRelations;
+    }
+
+    /**
+     * 来源2:查询 demand_pool_promotion_source 中已完成的晋级记录(level=利用池, status=FINISH)
+     * 三步:逐表查晋级记录 → 逐 root_produce_content_id 查 experiment_id → 回填并过滤
+     * <p>
+     * 返回仅包含成功回填 experiment_id 的记录
+     */
+    private List<DemandSearchArticleRelationDTO> fetchPromotionRelations() {
+        // Step 1: 查晋级表 demand_pool_promotion_source(独立于老晋级表)
+        String utilizeLevel = ContentPoolEnum.accountDemandPoolLevelUtilize.getContentPool();
+        List<DemandSearchArticleRelationDTO> promotionRelations = longArticleBaseMapper
+                .getDemandPoolPromotionByLevel(utilizeLevel,
+                        ArticlePoolPromotionSourceStatusEnum.FINISH.getCode(), StatusEnum.ZERO.getCode());
+        log.info("syncDemandProduceContentRelation source2(promotion) found {} relations",
+                promotionRelations != null ? promotionRelations.size() : 0);
+
+        if (CollectionUtils.isEmpty(promotionRelations)) {
+            return new ArrayList<>();
+        }
+
+        // Step 2: 按 account_id 分组 root_produce_content_id,查 demand_produce_content_relation 获取 experiment_id
+        // account_id → Set<rootProduceContentId>
+        Map<String, Set<String>> accountContentIds = new HashMap<>();
+        for (DemandSearchArticleRelationDTO pr : promotionRelations) {
+            if (StringUtils.hasText(pr.getContentId()) && StringUtils.hasText(pr.getAccountId())) {
+                accountContentIds.computeIfAbsent(pr.getAccountId(), k -> new HashSet<>()).add(pr.getContentId());
+            }
+        }
+        Map<String, String> contentIdToExperimentId = fetchExperimentIdByContentIds(accountContentIds);
+        log.info("syncDemandProduceContentRelation source2 found {} experiment_id by rootProduceContentId",
+                contentIdToExperimentId.size());
+
+        // Step 3: 回填 experiment_id,仅保留有匹配的记录
+        List<DemandSearchArticleRelationDTO> enriched = new ArrayList<>();
+        for (DemandSearchArticleRelationDTO pr : promotionRelations) {
+            String expId = contentIdToExperimentId.get(pr.getContentId());
+            if (expId != null) {
+                pr.setExperimentId(expId);
+                enriched.add(pr);
+            }
+        }
+        return enriched;
+    }
+
+    /**
+     * 按账号粒度查询 demand_produce_content_relation 获取 experiment_id
+     * <p>
+     * 应用场景:晋级记录有 root_produce_content_id + account_id,反向查 experiment_id
+     *
+     * @param accountContentIds account_id → Set<content_id> 分组
+     */
+    private Map<String, String> fetchExperimentIdByContentIds(Map<String, Set<String>> accountContentIds) {
+        Map<String, String> result = new HashMap<>();
+        if (accountContentIds == null || accountContentIds.isEmpty()) {
+            return result;
+        }
+        for (Map.Entry<String, Set<String>> entry : accountContentIds.entrySet()) {
+            String accountId = entry.getKey();
+            List<String> contentIds = new ArrayList<>(entry.getValue());
+            for (List<String> partition : Lists.partition(contentIds, 500)) {
+                List<DemandSearchArticleRelationDTO> expMappings =
+                        longArticleBaseMapper.getDemandExperimentIdByContentIds(partition, accountId);
+                if (CollectionUtils.isNotEmpty(expMappings)) {
+                    for (DemandSearchArticleRelationDTO m : expMappings) {
+                        if (StringUtils.hasText(m.getContentId()) && StringUtils.hasText(m.getExperimentId())) {
+                            result.put(m.getContentId(), m.getExperimentId());
+                        }
+                    }
+                }
+            }
+        }
+        return result;
+    }
+
+    /**
+     * 跨库查询:去 aigc 数据库批量查 produce_plan_exe_record
+     * <p>
+     * 输入:所有来源中包含的 channel_content_id(去重)
+     * 输出:channel_content_id → plan_exe_id 映射
+     * 分批大小 1000,避免 IN 子句过长
+     */
+    private Map<String, String> fetchPlanExeIdByChannelContentIds(List<DemandSearchArticleRelationDTO> relations) {
+        // 提取并去重 channel_content_id
+        List<String> channelContentIds = relations.stream()
+                .map(DemandSearchArticleRelationDTO::getChannelContentId)
+                .filter(StringUtils::hasText)
+                .distinct()
+                .collect(Collectors.toList());
+
+        // 分批查询 produce_plan_exe_record(每次最多 1000 个)
+        Map<String, String> result = new HashMap<>();
+        for (List<String> partition : Lists.partition(channelContentIds, 1000)) {
+            List<ProducePlanExeRecord> records = aigcBaseMapper.getProducePlanExeRecordByChannelContentIds(partition);
+            if (CollectionUtils.isNotEmpty(records)) {
+                for (ProducePlanExeRecord record : records) {
+                    if (StringUtils.hasText(record.getChannelContentId())) {
+                        result.put(record.getChannelContentId(), record.getPlanExeId());
+                    }
+                }
+            }
+        }
+        log.info("syncDemandProduceContentRelation found {} plan_exe_id mappings from produce_plan_exe_record",
+                result.size());
+        return result;
+    }
+
+    /**
+     * 装配最终写入的三元组 (content_id=plan_exe_id, channel_content_id, experiment_id)
+     * <p>
+     * 过滤规则:
+     * - experiment_id 为空 → 跳过(来源2 中未匹配到 experiment_id 的晋级记录)
+     * - channel_content_id 在 aigc 库查不到 plan_exe_id → 跳过(抓取内容尚未生成)
+     */
+    private List<DemandSearchArticleRelationDTO> buildInsertList(List<DemandSearchArticleRelationDTO> relations,
+                                                                  Map<String, String> channelContentIdToPlanExeId) {
+        List<DemandSearchArticleRelationDTO> insertList = new ArrayList<>();
+        for (DemandSearchArticleRelationDTO relation : relations) {
+            // 必须有 experiment_id
+            if (!StringUtils.hasText(relation.getExperimentId())) {
+                continue;
+            }
+            // 必须在 aigc 库找到对应的 plan_exe_id
+            String planExeId = channelContentIdToPlanExeId.get(relation.getChannelContentId());
+            if (StringUtils.hasText(planExeId)) {
+                relation.setContentId(planExeId);
+                insertList.add(relation);
+            }
+        }
+        log.info("syncDemandProduceContentRelation enriched {} records to insert", insertList.size());
+        return insertList;
+    }
+
+    /**
+     * INSERT IGNORE 批量写入 demand_produce_content_relation
+     * <p>
+     * 唯一键 uk_exp_channel_content 保证幂等,重复记录自动跳过
+     * 分批 500 条,控制单次 SQL 大小
+     */
+    private void batchInsertDemandRelation(List<DemandSearchArticleRelationDTO> insertList) {
+        for (List<DemandSearchArticleRelationDTO> partition : Lists.partition(insertList, 500)) {
+            longArticleBaseMapper.batchInsertIgnoreDemandProduceContentRelation(partition);
+        }
+    }
+
 }

+ 4 - 2
long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/service/recommend/ArticleCategoryService.java

@@ -188,7 +188,8 @@ public class ArticleCategoryService {
         List<ProducePlan> producePlanList = producePlanRepository.findByIdIn(producePlanIds);
         producePlanIds = producePlanList.stream()
                 .filter(o -> StringUtils.hasText(o.getPlanTag())
-                        && o.getPlanTag().contains(ContentPoolEnum.autoArticlePoolLevel4.getContentPool()))
+                        && (o.getPlanTag().contains(ContentPoolEnum.autoArticlePoolLevel4.getContentPool())
+                        || o.getPlanTag().contains(ContentPoolEnum.accountDemandPoolLevelCold.getContentPool())))
                 .map(ProducePlan::getId).collect(Collectors.toList());
         List<ArticleCategory> saveList = addArticleCategoryByProducePlan(producePlanIds);
         if (CollectionUtils.isNotEmpty(saveList)) {
@@ -204,7 +205,8 @@ public class ArticleCategoryService {
                 .filter(o -> StringUtils.hasText(o.getPlanTag())
                         && (o.getPlanTag().contains(ContentPoolEnum.autoArticlePoolLevel1.getContentPool())
                         || o.getPlanTag().contains(ContentPoolEnum.autoArticlePoolLevel2.getContentPool())
-                        || o.getPlanTag().contains(ContentPoolEnum.autoArticlePoolLevel3.getContentPool())))
+                        || o.getPlanTag().contains(ContentPoolEnum.autoArticlePoolLevel3.getContentPool())
+                        || o.getPlanTag().contains(ContentPoolEnum.accountDemandPoolLevelUtilize.getContentPool())))
                 .map(ProducePlan::getId).collect(Collectors.toList());
 
         List<ArticleCategory> saveList = addArticleCategoryByProducePlan(articlePromotionProducePlanIds);

+ 267 - 19
long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/service/recommend/ArticlePromotionService.java

@@ -29,6 +29,7 @@ import com.tzld.longarticle.recommend.server.model.vo.aigc.ProducePlanInputSourc
 import com.tzld.longarticle.recommend.server.remote.WxFetchRemoteService;
 import com.tzld.longarticle.recommend.server.remote.aigc.AIGCCrawlerPlanSaveService;
 import com.tzld.longarticle.recommend.server.remote.aigc.AIGCProduceContentListService;
+import com.tzld.longarticle.recommend.server.remote.aigc.AIGCProducePlanCreateService;
 import com.tzld.longarticle.recommend.server.remote.aigc.AIGCProducePlanDetailService;
 import com.tzld.longarticle.recommend.server.remote.aigc.AIGCProducePlanSaveService;
 import com.tzld.longarticle.recommend.server.repository.aigc.PublishAccountRepository;
@@ -90,6 +91,14 @@ public class ArticlePromotionService {
     VideoTitleReWriteRepository videoTitleReWriteRepository;
     @Autowired
     ArticleUnsafeTitleRepository articleUnsafeTitleRepository;
+    @Autowired
+    DemandPoolPromotionSourceRepository demandPoolPromotionSourceRepository;
+    @Autowired
+    AccountDemandUtilizePlanRepository accountDemandUtilizePlanRepository;
+    @Autowired
+    DemandPoolPromotionBindRepository demandPoolPromotionBindRepository;
+    @Autowired
+    AIGCProducePlanCreateService aigcProducePlanCreateService;
 
     @ApolloJsonValue("${articlePromotionProduceConfig:{}}")
     private Map<String, Map<String, Map<String, String>>> produceConfig;
@@ -98,6 +107,11 @@ public class ArticlePromotionService {
     @Value("${readOpenFissionRateThreshold:}")
     private Double readOpenFissionRateThreshold;
 
+    /**
+     * 所有账号共用一个生成计划的哨兵键,替代原来的按账号区分。
+     */
+    public static final String SHARED_ACCOUNT_KEY = "SHARED";
+
     private final List<String> contentPoolType = Arrays.asList("autoArticlePoolLevel1", "autoArticlePoolLevel3", "autoArticlePoolLevel4");
 
     /**
@@ -175,6 +189,10 @@ public class ArticlePromotionService {
         // 获取内部表现
         List<DatastatSortStrategy> list;
         list = longArticleBaseMapper.getArticlePromotionCandidates(10000, dateStrFilter, positionFilter);
+        // 需求内容池晋级过滤
+        list = list.stream().filter(o -> !o.getProducePlanTag().contains(ContentPoolEnum.accountDemandPoolLevelCold.getContentPool())
+                        && !o.getProducePlanTag().contains(ContentPoolEnum.accountDemandPoolLevelUtilize.getContentPool()))
+                .collect(Collectors.toList());
         // 使用阅读均值倍数+阅读量晋级
         List<DatastatSortStrategy> listStrategyV1 = list.stream()
                 .filter(o -> o.getReadRate() >= viewCountRateFilter && o.getViewCount() >= viewCountFilter)
@@ -207,6 +225,58 @@ public class ArticlePromotionService {
         filterAndAdd2CrawlerPlan(listStrategyV2, "策略V2", accountNickName, tag, pos, way, today);
     }
 
+    /**
+     * 需求匹配内容池晋级:冷启池 → 利用池
+     * 从 datastat_sort_strategy 中筛选 produce_plan_tag 包含 accountDemandPoolLevelCold 的高表现内容,
+     * 晋级到 accountDemandPoolLevelUtilize
+     */
+    public void demandPoolPromotion(Integer viewCountFilter, Double viewCountRateFilter,
+                                     List<Integer> positionFilter) {
+        String dateStrFilter = DateUtils.getBeforeDaysDateStr("yyyyMMdd", 10);
+        List<DatastatSortStrategy> list = longArticleBaseMapper.getArticlePromotion(viewCountFilter, viewCountRateFilter,
+                10000, dateStrFilter, positionFilter);
+        // 仅保留需求冷启池内容
+        list = list.stream()
+                .filter(o -> o.getProducePlanTag() != null
+                        && o.getProducePlanTag().contains(ContentPoolEnum.accountDemandPoolLevelCold.getContentPool()))
+                .collect(Collectors.toList());
+        if (CollectionUtils.isEmpty(list)) {
+            log.info("demandPoolPromotion 需求冷启池无优质文章");
+            return;
+        }
+        list = filterEarlyContent(list, true);
+        log.info("demandPoolPromotion 需求冷启池优质文章数量: {}", list.size());
+        List<DatastatSortStrategy> distinct = filterSameTitle(list);
+        distinct.sort(Comparator.comparing(DatastatSortStrategy::getDateStr, Comparator.reverseOrder()));
+        log.info("demandPoolPromotion 需求冷启池优质文章数量(去重后): {}", distinct.size());
+
+        // 保存晋级记录:冷启池 → 利用池,写入新表 demand_pool_promotion_source(与老晋级表分离)
+        // 先批量查询 gh_id → account_id 映射
+        List<String> ghIds = distinct.stream()
+                .map(DatastatSortStrategy::getGhId)
+                .distinct()
+                .collect(Collectors.toList());
+        List<PublishAccount> publishAccounts = publishAccountRepository.getAllByGhIdIn(ghIds);
+        Map<String, String> ghIdToAccountId = publishAccounts.stream()
+                .collect(Collectors.toMap(PublishAccount::getGhId, PublishAccount::getId, (a, b) -> a));
+
+        int promotionNum = 0;
+        for (DatastatSortStrategy item : distinct) {
+            String wxSn = item.getWxSn();
+            String title = item.getTitle();
+            String accountId = ghIdToAccountId.get(item.getGhId());
+            String publishContentId = articleService.getPublishContentByWxSn(wxSn);
+            if (StringUtils.hasText(publishContentId)) {
+                saveDemandPoolPromotionSource(Md5Util.encoderByMd5(publishContentId), wxSn, title,
+                        ContentPoolEnum.accountDemandPoolLevelUtilize.getContentPool(), accountId);
+                promotionNum++;
+            }
+        }
+        log.info("demandPoolPromotion 晋级完成,晋级数量: {}", promotionNum);
+        FeishuMessageSender.sendWebHookMessage(FeishuRobotIdEnum.DAILY.getRobotId(),
+                "【需求内容池晋级任务完成】\n晋级数量:" + promotionNum);
+    }
+
     private void filterAndAdd2CrawlerPlan(List<DatastatSortStrategy> list, String promotionStrategy, String accountNickName, String tag, String pos, String way, String today) {
         list = filterEarlyContent(list, true);
         log.info("优质{}文章数量: {}", accountNickName, list.size());
@@ -289,25 +359,8 @@ public class ArticlePromotionService {
                 if (StringUtils.hasText(publishContentId)) {
                     publishContentIds.add(publishContentId);
                     saveArticlePoolPromotionSource(Md5Util.encoderByMd5(publishContentId), wxSn, title, level);
-//                } else {
-//                    filterUrlList.add(url);
-//                    // 调用爬虫 detail 接口并保存数据
-//                    WxContentDetailResponse detail = getArticleDetail(url);
-//                    if (detail != null && StringUtils.hasText(detail.getChannelContentId())) {
-//                        saveArticlePoolPromotionSource(detail.getChannelContentId(), wxSn, title, level);
-//                    }
                 }
             }
-//            if (CollectionUtils.isNotEmpty(filterUrlList)) {
-//                String planName = String.format("%d_%s_%s_%s【%s】_%s_%s", filterUrlList.size(), today, accountNickName, pos, way, today, promotionStrategy);
-//                log.info("url_len: " + list.size() + ", " + filterUrlList.size());
-//                IdNameVO<String> planInfo = aigcCrawlerPlanSaveService.createArticleUrlPlan(planName, filterUrlList, tag, CrawlerModeEnum.ContentIDs.getVal());
-//                if (StringUtils.hasText(produceId)) {
-//                    String inputSourceLabel = String.format("原始帖子-长文-微信公众号-内容添加计划-%s", planInfo.getName());
-//                    articleAddDependPlan(produceId, planInfo.getId(), inputSourceLabel, ProducePlanInputSourceTypeEnum.contentPlan.getVal());
-//                }
-//                log.info("{}, {}, produce plan not exist: {}, {}, {}", planInfo.getName(), planInfo.getId(), accountNickName, pos, way);
-//            }
             if (CollectionUtils.isNotEmpty(publishContentIds)) {
                 String planName = String.format("%d_%s_%s_%s【%s】_%s_%s", publishContentIds.size(), today, accountNickName, pos, way, today, promotionStrategy);
                 IdNameVO<String> planInfo = aigcCrawlerPlanSaveService.createArticleUrlPlan(planName, publishContentIds, tag, CrawlerModeEnum.PublishContentIds.getVal());
@@ -329,11 +382,10 @@ public class ArticlePromotionService {
 
     private void sendFeishuJobFinishMessage(String accountNickName, Integer urlListSize, Integer contentListSize, String promotionStrategy) {
         log.info("articlePromotion finish: 晋级任务:{}, id晋级数量:{}, url晋级数量:{}", accountNickName, contentListSize, urlListSize);
-        FeishuMessageSender.sendWebHookMessage(FeishuRobotIdEnum.DAILY.getRobotId(),
+        FeishuMessageSender.sendWebHookMessage(FeishuRobotIdEnum.RECOMMEND.getRobotId(),
                 "【文章晋级job完成】\n" +
                         "晋级任务:" + accountNickName + "\n" +
                         "id晋级数量:" + contentListSize + "\n" +
-//                        "url晋级数量:" + urlListSize + "\n" +
                         "晋级策略: " + promotionStrategy + "\n");
     }
 
@@ -468,6 +520,28 @@ public class ArticlePromotionService {
         articlePoolPromotionSourceRepository.save(articlePromotion);
     }
 
+    /**
+     * 保存需求池晋级记录到独立表 demand_pool_promotion_source
+     */
+    private void saveDemandPoolPromotionSource(String channelContentId, String wxSn, String title,
+                                                String level, String accountId) {
+        DemandPoolPromotionSource source = demandPoolPromotionSourceRepository.getByChannelContentId(channelContentId);
+        if (Objects.isNull(source)) {
+            source = new DemandPoolPromotionSource();
+            source.setChannelContentId(channelContentId);
+            source.setTitle(title);
+            source.setTitleMd5(Md5Util.encoderByMd5(title));
+            source.setCreateTimestamp(System.currentTimeMillis());
+            source.setStatus(ArticlePoolPromotionSourceStatusEnum.WAITING.getCode());
+            source.setDeleted(StatusEnum.ZERO.getCode());
+        }
+        source.setWxSn(wxSn);
+        source.setLevel(level);
+        source.setAccountId(accountId);
+        source.setUpdateTimestamp(System.currentTimeMillis());
+        demandPoolPromotionSourceRepository.save(source);
+    }
+
     private void articleAddDependPlan(String produceId, String inputSourceValue, String inputSourceLabel, Integer inputSourceType) {
         try {
             // 获取生产计划的详细信息
@@ -722,4 +796,178 @@ public class ArticlePromotionService {
         return detail;
     }
 
+    /**
+     * 需求池晋级绑定:将已完成溯源的晋级记录绑定到 accountDemandPoolLevelUtilize 生成计划。
+     * <p>
+     * 所有账号共用一个生成计划,通过 demand_pool_promotion_bind.account_id 区分归属。
+     * <p>
+     * 流程:
+     * 1. 读取 demand_pool_promotion_source(status=FINISH, 未绑定)
+     * 2. 查找/创建共用的 accountDemandPoolLevelUtilize 生成计划
+     * 3. 对每条记录获取 publishContentId → 创建抓取计划 → 绑定到共用生成计划
+     */
+    public void demandPoolPromotionBind() {
+        List<DemandPoolPromotionSource> finishedRecords = demandPoolPromotionSourceRepository
+                .getByStatusAndDeleted(ArticlePoolPromotionSourceStatusEnum.FINISH.getCode(), StatusEnum.ZERO.getCode());
+        if (CollectionUtils.isEmpty(finishedRecords)) {
+            log.info("demandPoolPromotionBind no finished records");
+            return;
+        }
+
+        String tag = ContentPoolEnum.accountDemandPoolLevelUtilize.getContentPool();
+        int bindCount = 0;
+        int skipCount = 0;
+
+        // 所有账号共用一个生成计划
+        String sharedProducePlanId = null;
+        boolean sharedPlanInitialized = false;
+        AccountDemandUtilizePlan sharedMapping = accountDemandUtilizePlanRepository.getByAccountId(SHARED_ACCOUNT_KEY);
+        if (sharedMapping != null && StringUtils.hasText(sharedMapping.getProducePlanId())) {
+            ProducePlanDetailVO existDetail = getProducePlanDetailRetry(sharedMapping.getProducePlanId());
+            if (existDetail != null) {
+                sharedProducePlanId = sharedMapping.getProducePlanId();
+                sharedPlanInitialized = true;
+                log.info("demandPoolPromotionBind found existing shared plan, planId:{}", sharedProducePlanId);
+            } else {
+                log.warn("demandPoolPromotionBind shared plan not found, will recreate. oldPlanId:{}",
+                        sharedMapping.getProducePlanId());
+            }
+        }
+
+        for (DemandPoolPromotionSource record : finishedRecords) {
+            try {
+                // 已绑定则跳过
+                DemandPoolPromotionBind existingBind = demandPoolPromotionBindRepository
+                        .getByPromotionChannelContentIdAndAccountId(record.getChannelContentId(), record.getAccountId());
+                if (existingBind != null) {
+                    skipCount++;
+                    continue;
+                }
+
+                String accountId = record.getAccountId();
+                if (!StringUtils.hasText(accountId)) {
+                    log.warn("demandPoolPromotionBind skip: empty accountId, channelContentId:{}", record.getChannelContentId());
+                    skipCount++;
+                    continue;
+                }
+
+                // 1. 获取 publishContentId
+                String wxSn = record.getWxSn();
+                String publishContentId = null;
+                if (StringUtils.hasText(wxSn)) {
+                    publishContentId = articleService.getPublishContentByWxSn(wxSn);
+                }
+                // fallback: 使用 sourcePublishContentId
+                if (!StringUtils.hasText(publishContentId) && StringUtils.hasText(record.getSourcePublishContentId())) {
+                    publishContentId = record.getSourcePublishContentId();
+                }
+                if (!StringUtils.hasText(publishContentId)) {
+                    log.warn("demandPoolPromotionBind skip: no publishContentId, title:{}", record.getTitle());
+                    skipCount++;
+                    continue;
+                }
+                String accountName = getAccountNameFromRecord(record);
+
+                // 2. 创建抓取计划
+                List<String> contentIds = Collections.singletonList(publishContentId);
+                String crawlerPlanName = String.format("需求池晋级_%s_%s", accountName, DateUtils.getCurrentDateStr("yyyyMMdd"));
+                IdNameVO<String> crawlerPlanInfo = aigcCrawlerPlanSaveService.createArticleUrlPlan(
+                        crawlerPlanName, contentIds, tag, CrawlerModeEnum.PublishContentIds.getVal());
+                if (crawlerPlanInfo == null || !StringUtils.hasText(crawlerPlanInfo.getId())) {
+                    log.warn("demandPoolPromotionBind create crawler plan failed, publishContentId:{}", publishContentId);
+                    skipCount++;
+                    continue;
+                }
+                long now = System.currentTimeMillis();
+
+                // 3. 共用生成计划:首次创建,后续记录绑定到同一个计划
+                if (!sharedPlanInitialized) {
+                    // 创建共用生成计划(抓取计划 ID 直接注入输入源)
+                    IdNameVO<String> planInfo = aigcProducePlanCreateService.createUtilizeProducePlan(
+                            accountName, crawlerPlanInfo.getId(), crawlerPlanInfo.getName());
+                    if (planInfo == null || !StringUtils.hasText(planInfo.getId())) {
+                        log.error("demandPoolPromotionBind create shared produce plan failed, accountName:{}", accountName);
+                        skipCount++;
+                        continue;
+                    }
+                    sharedProducePlanId = planInfo.getId();
+                    sharedPlanInitialized = true;
+
+                    if (sharedMapping == null) {
+                        sharedMapping = new AccountDemandUtilizePlan();
+                    }
+                    sharedMapping.setAccountId(SHARED_ACCOUNT_KEY);
+                    sharedMapping.setProducePlanId(planInfo.getId());
+                    sharedMapping.setPlanName(planInfo.getName());
+                    sharedMapping.setCreatedAt(now);
+                    sharedMapping.setUpdatedAt(now);
+                    accountDemandUtilizePlanRepository.save(sharedMapping);
+
+                    log.info("demandPoolPromotionBind created shared plan, planId:{} crawlerPlanId:{}",
+                            sharedProducePlanId, crawlerPlanInfo.getId());
+                } else {
+                    // 绑定抓取计划到已有共用生成计划
+                    String inputSourceLabel = String.format("原始帖子-长文-微信公众号-内容添加计划-%s", crawlerPlanInfo.getName());
+                    articleAddDependPlan(sharedProducePlanId, crawlerPlanInfo.getId(), inputSourceLabel,
+                            ProducePlanInputSourceTypeEnum.contentPlan.getVal());
+                    log.info("demandPoolPromotionBind bind to shared plan, accountId:{} planId:{} crawlerPlanId:{}",
+                            accountId, sharedProducePlanId, crawlerPlanInfo.getId());
+                }
+
+                if (sharedProducePlanId == null) {
+                    FeishuMessageSender.sendWebHookMessage(FeishuRobotIdEnum.JOB.getRobotId(),
+                            String.format("【需求池利用池生成计划创建失败】\n账号ID:%s\n账号名:%s\n晋级标题:%s\n<at user_id=\"g6732afb\">王云鹏</at>",
+                                    accountId, accountName, record.getTitle()));
+                }
+
+                // 4. 记录绑定关系(accountId 区分归属)
+                DemandPoolPromotionBind bind = new DemandPoolPromotionBind();
+                bind.setPromotionChannelContentId(record.getChannelContentId());
+                bind.setAccountId(accountId);
+                bind.setRootProduceContentId(record.getRootProduceContentId());
+                bind.setCrawlerPlanId(crawlerPlanInfo.getId());
+                bind.setProducePlanId(sharedProducePlanId);
+                bind.setCreatedAt(now);
+                bind.setUpdatedAt(now);
+                demandPoolPromotionBindRepository.save(bind);
+
+                bindCount++;
+                log.info("demandPoolPromotionBind success, channelContentId:{} accountId:{} crawlerPlanId:{} producePlanId:{}",
+                        record.getChannelContentId(), accountId, crawlerPlanInfo.getId(), sharedProducePlanId);
+
+            } catch (Exception e) {
+                log.error("demandPoolPromotionBind error, channelContentId:{} title:{}",
+                        record.getChannelContentId(), record.getTitle(), e);
+            }
+        }
+
+        log.info("demandPoolPromotionBind done, total:{} bindSuccess:{} skip:{}",
+                finishedRecords.size(), bindCount, skipCount);
+    }
+
+    private String getAccountNameFromRecord(DemandPoolPromotionSource record) {
+        if (!StringUtils.hasText(record.getAccountId())) {
+            return "unknown";
+        }
+        PublishAccount account = publishAccountRepository.getById(record.getAccountId());
+        return account != null && StringUtils.hasText(account.getName()) ? account.getName() : "unknown";
+    }
+
+    private ProducePlanDetailVO getProducePlanDetailRetry(String planId) {
+        int retryTimes = 3;
+        while (retryTimes > 0) {
+            ProducePlanDetailVO detail = aigcProducePlanDetailService.articleGetProducePlanDetail(planId);
+            if (detail != null) {
+                return detail;
+            }
+            retryTimes--;
+            try {
+                Thread.sleep(2000);
+            } catch (InterruptedException e) {
+                log.error("getProducePlanDetailRetry sleep error", e);
+            }
+        }
+        return null;
+    }
+
 }

+ 64 - 0
long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/service/recommend/ArticleService.java

@@ -17,6 +17,7 @@ import com.tzld.longarticle.recommend.server.model.entity.aigc.PublishContent;
 import com.tzld.longarticle.recommend.server.model.entity.aigc.PublishContentOutput;
 import com.tzld.longarticle.recommend.server.model.entity.crawler.Article;
 import com.tzld.longarticle.recommend.server.model.entity.longArticle.ArticlePoolPromotionSource;
+import com.tzld.longarticle.recommend.server.model.entity.longArticle.DemandPoolPromotionSource;
 import com.tzld.longarticle.recommend.server.model.entity.longArticle.LongArticlesRootSourceId;
 import com.tzld.longarticle.recommend.server.model.param.ArticleFindSourceParam;
 import com.tzld.longarticle.recommend.server.model.vo.RootPublishContentVO;
@@ -24,6 +25,7 @@ import com.tzld.longarticle.recommend.server.repository.aigc.PublishAccountRepos
 import com.tzld.longarticle.recommend.server.repository.aigc.PublishContentOutputRepository;
 import com.tzld.longarticle.recommend.server.repository.crawler.ArticleRepository;
 import com.tzld.longarticle.recommend.server.repository.longArticle.ArticlePoolPromotionSourceRepository;
+import com.tzld.longarticle.recommend.server.repository.longArticle.DemandPoolPromotionSourceRepository;
 import com.tzld.longarticle.recommend.server.repository.longArticle.LongArticlesRootSourceIdRepository;
 import com.tzld.longarticle.recommend.server.util.DateUtils;
 import com.tzld.longarticle.recommend.server.util.TitleSimilarCheckUtil;
@@ -67,6 +69,8 @@ public class ArticleService {
     @Autowired
     ArticlePoolPromotionSourceRepository articlePoolPromotionSourceRepository;
     @Autowired
+    DemandPoolPromotionSourceRepository demandPoolPromotionSourceRepository;
+    @Autowired
     LongArticlesRootSourceIdRepository longArticlesRootSourceIdRepository;
 
     private final static ExecutorService pool = new CommonThreadPoolExecutor(
@@ -355,6 +359,66 @@ public class ArticleService {
         }
     }
 
+    /**
+     * 需求池晋级溯源:处理 demand_pool_promotion_source 中待溯源的记录
+     * 与 articlePromotionTraceability 逻辑相同,但操作独立晋级表
+     */
+    public void demandPoolPromotionTraceability(String channelContentId) {
+        List<DemandPoolPromotionSource> tasks = new ArrayList<>();
+        if (StringUtils.hasText(channelContentId)) {
+            DemandPoolPromotionSource item = demandPoolPromotionSourceRepository.getByChannelContentId(channelContentId);
+            tasks.add(item);
+        } else {
+            tasks = demandPoolPromotionSourceRepository.getByStatusAndDeleted(0, 0);
+        }
+        long now = System.currentTimeMillis();
+        for (DemandPoolPromotionSource task : tasks) {
+            try {
+                // 判断文章是否被抓回来
+                CrawlerContent crawlerContent = aigcBaseMapper.getCrawlerContentByChannelContentId(task.getChannelContentId());
+                if (Objects.isNull(crawlerContent)) {
+                    continue;
+                }
+                // 溯源
+                Article article = articleRepository.getByWxSn(task.getWxSn());
+                if (Objects.isNull(article)) {
+                    task.setDeleted(StatusEnum.ONE.getCode());
+                    demandPoolPromotionSourceRepository.save(task);
+                    continue;
+                }
+                PublishAccount publishAccount = publishAccountRepository.getByGhId(article.getGhId());
+                if (Objects.isNull(publishAccount)) {
+                    continue;
+                }
+                long publishTimestamp = article.getPublishTimestamp() > 0 ? article.getPublishTimestamp() * 1000 : article.getUpdateTime() * 1000;
+                List<PublishContent> publishContentList = publishContentMapper.getNearestPublishContent(publishAccount.getId(), publishTimestamp, 100);
+                PublishContent publishContent = findPublishContent(publishContentList, task.getTitle(), publishTimestamp);
+                if (Objects.isNull(publishContent)) {
+                    publishContentList = publishContentMapper.getLateNearestPublishContent(publishAccount.getId(), publishTimestamp);
+                    publishContent = findPublishContent(publishContentList, task.getTitle(), publishTimestamp);
+                    if (Objects.isNull(publishContent)) {
+                        task.setDeleted(StatusEnum.ONE.getCode());
+                        demandPoolPromotionSourceRepository.save(task);
+                        continue;
+                    }
+                }
+                RootPublishContentVO source = getRootPublishContent(publishContent.getCrawlerChannelContentId(), null, publishContent.getId(), publishContent.getSourceId(), 0);
+                if (StringUtils.hasText(source.getRootProduceContentId())) {
+                    task.setStatus(ArticlePoolPromotionSourceStatusEnum.FINISH.getCode());
+                    task.setSourcePublishContentId(source.getSourcePublishContentId());
+                    task.setRootProduceContentId(source.getRootProduceContentId());
+                    task.setUpdateTimestamp(now);
+                    demandPoolPromotionSourceRepository.save(task);
+                } else {
+                    task.setDeleted(StatusEnum.ONE.getCode());
+                    demandPoolPromotionSourceRepository.save(task);
+                }
+            } catch (Exception e) {
+                log.error("demandPoolPromotionTraceability error channelContentId:{}", task.getChannelContentId(), e);
+            }
+        }
+    }
+
     public String getPublishContentByWxSn(String wxSn) {
         Article article = articleRepository.getByWxSn(wxSn);
         PublishAccount publishAccount = publishAccountRepository.getByGhId(article.getGhId());

+ 2 - 1
long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/service/recommend/ArticleVideoAuditService.java

@@ -374,7 +374,8 @@ public class ArticleVideoAuditService {
         int index = random.nextInt(auditUser.size());
         String auditAccount = auditUser.get(index);
         int status = ArticleVideoAuditStatusEnum.WAITING.getCode();
-        if (!Objects.equals(param.getFlowPoolLevel(), ContentPoolEnum.autoArticlePoolLevel4.getContentPool())) {
+        if (!Objects.equals(param.getFlowPoolLevel(), ContentPoolEnum.autoArticlePoolLevel4.getContentPool())
+                && !Objects.equals(param.getFlowPoolLevel(), ContentPoolEnum.accountDemandPoolLevelCold.getContentPool())) {
             if (Objects.nonNull(exeRecord)) {
                 String channelContentId = exeRecord.getChannelContentId();
                 ArticlePoolPromotionSource source = articlePoolPromotionSourceRepository.getByChannelContentId(channelContentId);

+ 2 - 1
long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/service/recommend/DataDashboardService.java

@@ -2600,7 +2600,8 @@ public class DataDashboardService {
         }
         Map<String, ContentPoolTypeDTO> poolTypeMap = poolTypeDTOS.stream().collect(Collectors.toMap(ContentPoolTypeDTO::getContentId, o -> o));
         Map<String, VideoAuditExport> map = new HashMap<>();
-        ContentPoolEnum[] poolEnums = {ContentPoolEnum.autoArticlePoolLevel1, ContentPoolEnum.autoArticlePoolLevel3, ContentPoolEnum.autoArticlePoolLevel4};
+        ContentPoolEnum[] poolEnums = {ContentPoolEnum.autoArticlePoolLevel1, ContentPoolEnum.autoArticlePoolLevel3, ContentPoolEnum.autoArticlePoolLevel4,
+                ContentPoolEnum.accountDemandPoolLevelCold, ContentPoolEnum.accountDemandPoolLevelUtilize};
         for (ContentPoolEnum contentPool : poolEnums) {
             VideoAuditExport videoAuditExport = new VideoAuditExport();
             videoAuditExport.setDateStr(dateStr);

+ 142 - 0
long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/service/recommend/FilterContentByAccountService.java

@@ -0,0 +1,142 @@
+package com.tzld.longarticle.recommend.server.service.recommend;
+
+import com.tzld.longarticle.recommend.server.mapper.longArticle.LongArticleBaseMapper;
+import com.tzld.longarticle.recommend.server.model.dto.DemandSearchArticleRelationDTO;
+import com.tzld.longarticle.recommend.server.model.entity.aigc.PublishAccount;
+import com.tzld.longarticle.recommend.server.model.entity.longArticle.DemandPoolPromotionBind;
+import com.tzld.longarticle.recommend.server.repository.aigc.PublishAccountRepository;
+import com.tzld.longarticle.recommend.server.repository.longArticle.DemandPoolPromotionBindRepository;
+import com.google.common.collect.Lists;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 内容是否属于任一账号的过滤查询服务
+ *
+ * @author dyp
+ */
+@Service
+@Slf4j
+public class FilterContentByAccountService {
+
+    @Autowired
+    private LongArticleBaseMapper longArticleBaseMapper;
+
+    @Autowired
+    private DemandPoolPromotionBindRepository demandPoolPromotionBindRepository;
+
+    @Autowired
+    private PublishAccountRepository publishAccountRepository;
+
+    /**
+     * 根据 channelContentId 和账号ID查询内容是否属于任一账号。
+     * 判断标准:
+     * 1. 通过 channelContentId → experimentId → demand_search_queue.account → publish_account.id 判断所属账号
+     * 2. 通过 channelContentId 查询 demand_pool_promotion_bind,判断内容与所属账号
+     * 任一条件满足即视为通过。
+     *
+     * @param channelContentIds 内容渠道ID列表
+     * @param accountIds        账号ID列表
+     * @return 不在任一账号下的 channelContentId 列表(需要被过滤的内容)
+     */
+    public List<String> filterContentByAccount(List<String> channelContentIds, List<String> accountIds) {
+        if (CollectionUtils.isEmpty(channelContentIds) || CollectionUtils.isEmpty(accountIds)) {
+            return new ArrayList<>();
+        }
+
+        Set<String> accountIdSet = new HashSet<>(accountIds);
+        Set<String> passedChannelContentIds = new HashSet<>();
+
+        // 检查1:channelContentId → experimentId → demand_search_queue.account → publish_account.id
+        List<DemandSearchArticleRelationDTO> demandRelations =
+                longArticleBaseMapper.getDemandExperimentIdByChannelContentIds(channelContentIds);
+
+        if (!CollectionUtils.isEmpty(demandRelations)) {
+            // 构建 channelContentId → experimentId 映射
+            Map<String, String> ccIdToExpId = demandRelations.stream()
+                    .filter(d -> StringUtils.isNotBlank(d.getChannelContentId())
+                            && StringUtils.isNotBlank(d.getExperimentId()))
+                    .collect(Collectors.toMap(
+                            DemandSearchArticleRelationDTO::getChannelContentId,
+                            DemandSearchArticleRelationDTO::getExperimentId,
+                            (v1, v2) -> v1));
+
+            if (!ccIdToExpId.isEmpty()) {
+                // experiment_id → account(账号名称),从 demand_search_queue 查询
+                List<String> experimentIds = new ArrayList<>(new HashSet<>(ccIdToExpId.values()));
+                Map<String, String> expIdToAccountName = new HashMap<>();
+                for (List<String> partition : Lists.partition(experimentIds, 500)) {
+                    List<DemandSearchArticleRelationDTO> queueResults =
+                            longArticleBaseMapper.getAccountByExperimentIds(partition);
+                    if (!CollectionUtils.isEmpty(queueResults)) {
+                        for (DemandSearchArticleRelationDTO r : queueResults) {
+                            if (StringUtils.isNotBlank(r.getExperimentId()) && StringUtils.isNotBlank(r.getAccountId())) {
+                                expIdToAccountName.put(r.getExperimentId(), r.getAccountId());
+                            }
+                        }
+                    }
+                }
+
+                if (!expIdToAccountName.isEmpty()) {
+                    // account名称 → account_id,从 publish_account 查询
+                    List<String> accountNames = new ArrayList<>(new HashSet<>(expIdToAccountName.values()));
+                    Map<String, String> accountNameToId = new HashMap<>();
+                    for (List<String> partition : Lists.partition(accountNames, 500)) {
+                        List<PublishAccount> accounts = publishAccountRepository.findByNameIn(partition);
+                        if (!CollectionUtils.isEmpty(accounts)) {
+                            for (PublishAccount account : accounts) {
+                                if (StringUtils.isNotBlank(account.getName()) && StringUtils.isNotBlank(account.getId())) {
+                                    accountNameToId.put(account.getName(), account.getId());
+                                }
+                            }
+                        }
+                    }
+
+                    // 遍历 channelContentId → experimentId → accountName → accountId,判断是否属于目标账号
+                    for (Map.Entry<String, String> entry : ccIdToExpId.entrySet()) {
+                        String accountName = expIdToAccountName.get(entry.getValue());
+                        if (StringUtils.isNotBlank(accountName)) {
+                            String accountId = accountNameToId.get(accountName);
+                            if (StringUtils.isNotBlank(accountId) && accountIdSet.contains(accountId)) {
+                                passedChannelContentIds.add(entry.getKey());
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        // 检查2:通过 channelContentId 查询 demand_pool_promotion_bind,判断内容与所属账号
+        List<String> remainingIds = channelContentIds.stream()
+                .filter(id -> !passedChannelContentIds.contains(id))
+                .collect(Collectors.toList());
+
+        if (!CollectionUtils.isEmpty(remainingIds)) {
+            List<DemandPoolPromotionBind> binds = demandPoolPromotionBindRepository
+                    .findByPromotionChannelContentIdInAndAccountIdIn(remainingIds, accountIds);
+
+            if (!CollectionUtils.isEmpty(binds)) {
+                binds.stream()
+                        .map(DemandPoolPromotionBind::getPromotionChannelContentId)
+                        .filter(StringUtils::isNotBlank)
+                        .forEach(passedChannelContentIds::add);
+            }
+        }
+
+        // 返回未通过任一检查的 channelContentId(需要被过滤的内容)
+        List<String> filteredIds = channelContentIds.stream()
+                .filter(id -> !passedChannelContentIds.contains(id))
+                .collect(Collectors.toList());
+
+        log.info("filterContentByAccount, total:{}, passed:{}, filtered:{}",
+                channelContentIds.size(), passedChannelContentIds.size(), filteredIds.size());
+
+        return filteredIds;
+    }
+}

+ 2 - 1
long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/service/recommend/PublishContentFilterService.java

@@ -35,7 +35,8 @@ public class PublishContentFilterService {
                 continue;
             }
             boolean isDuplicate = false;
-            if (ContentPoolEnum.autoArticlePoolLevel4.getContentPool().equals(contentItem.getPoolLevel())) {
+            if (ContentPoolEnum.autoArticlePoolLevel4.getContentPool().equals(contentItem.getPoolLevel())
+                    || ContentPoolEnum.accountDemandPoolLevelCold.getContentPool().equals(contentItem.getPoolLevel())) {
                 isDuplicate = TitleSimilarCheckUtil.isDuplicateContentByCache(contentItem.getTitle(),
                         firstSecondTitleCache, TitleSimilarCheckUtil.SIMILARITY_THRESHOLD);
             }

+ 3 - 1
long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/service/recommend/RankContentScoreService.java

@@ -147,10 +147,12 @@ public class RankContentScoreService {
     private List<Content> filterHeadContents(List<Content> contents) {
         List<Content> result = new ArrayList<>();
         String coldStartPool = ContentPoolEnum.autoArticlePoolLevel4.getContentPool();
+        String demandColdStartPool = ContentPoolEnum.accountDemandPoolLevelCold.getContentPool();
 
         for (Content content : contents) {
             // 跳过冷启池内容
-            if (coldStartPool.equals(content.getContentPoolType())) {
+            if (coldStartPool.equals(content.getContentPoolType())
+                    || demandColdStartPool.equals(content.getContentPoolType())) {
                 continue;
             }
             result.add(content);

+ 641 - 0
long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/service/recommend/RecommendService.java

@@ -17,13 +17,16 @@ import com.tzld.longarticle.recommend.server.mapper.longArticle.LongArticleBaseM
 import com.tzld.longarticle.recommend.server.model.cgi.VideoArticleMatchRecord;
 import com.tzld.longarticle.recommend.server.model.cgi.VideoArticleMatchResponse;
 import com.tzld.longarticle.recommend.server.model.dto.Content;
+import com.tzld.longarticle.recommend.server.model.dto.DemandSearchArticleRelationDTO;
 import com.tzld.longarticle.recommend.server.model.dto.UserGroupCountDTO;
 import com.tzld.longarticle.recommend.server.model.entity.crawler.AccountAvgInfo;
 import com.tzld.longarticle.recommend.server.model.entity.crawler.PublishSortLog;
+import com.tzld.longarticle.recommend.server.model.param.PlanColdStartSortRequest;
 import com.tzld.longarticle.recommend.server.model.param.RecommendParam;
 import com.tzld.longarticle.recommend.server.model.param.RecommendRequest;
 import com.tzld.longarticle.recommend.server.model.vo.ArticleSortResponseData;
 import com.tzld.longarticle.recommend.server.model.vo.ArticleSortResponseDataItem;
+import com.tzld.longarticle.recommend.server.model.vo.PlanColdStartSortResponse;
 import com.tzld.longarticle.recommend.server.model.vo.RecommendResponse;
 import com.tzld.longarticle.recommend.server.model.vo.RecommendWithUserGroupResponse;
 import com.tzld.longarticle.recommend.server.remote.pq.VideoArticleMatchService;
@@ -42,6 +45,7 @@ import com.tzld.longarticle.recommend.server.util.CommonCollectionUtils;
 import com.tzld.longarticle.recommend.server.util.DateUtils;
 import com.tzld.longarticle.recommend.server.util.JSONUtils;
 import com.tzld.longarticle.recommend.server.util.RedisUtil;
+import com.tzld.longarticle.recommend.server.util.TitleSimilarCheckUtil;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.collections4.CollectionUtils;
 import org.springframework.beans.BeanUtils;
@@ -58,6 +62,7 @@ import java.util.*;
 import java.util.concurrent.CompletableFuture;
 import java.util.stream.Collectors;
 
+import static com.tzld.longarticle.recommend.server.common.constant.SceneConstants.DEMAND_POOL;
 import static com.tzld.longarticle.recommend.server.common.constant.SceneConstants.FWH_COLD_START;
 
 /**
@@ -101,6 +106,9 @@ public class RecommendService {
     @Value("${spring.profiles.active}")
     private String env;
 
+    @Value("${coldStart.readingAvgThreshold:100}")
+    private int coldStartReadingAvgThreshold;
+
 
     public RecommendResponse recommend(RecommendRequest request) {
         long start = System.currentTimeMillis();
@@ -340,6 +348,339 @@ public class RecommendService {
                 new RecommendWithUserGroupResponse.RecommendWithUserGroupData(rankList, filterList));
     }
 
+    public RecommendResponse recommendDemandPool(RecommendRequest request) {
+        long start = System.currentTimeMillis();
+
+        RecommendParam param = genRecommendParam(request, DEMAND_POOL);
+        param.setStrategy(RankStrategyEnum.DEMAND_POOL_STRATEGY.getStrategy());
+        log.info("recommendDemandPool genRecommendParam {}", JSONUtils.toJson(param));
+
+        RecallResult recallResult = recallService.recall(convertToRecallParam(param));
+
+        if (CollectionUtils.isEmpty(recallResult.getData())) {
+            RecommendResponse response = new RecommendResponse();
+            response.setCode(0);
+            response.setMsg("success");
+            return response;
+        }
+        long t2 = System.currentTimeMillis();
+        CostMonitor.logCost("RecommendDemandPool", "Recall", t2 - start);
+
+        // 召回后检查内容池是否为空
+        String utilizePool = ContentPoolEnum.accountDemandPoolLevelUtilize.getContentPool();
+        String coldPool = ContentPoolEnum.accountDemandPoolLevelCold.getContentPool();
+        boolean hasUtilize = false;
+        boolean hasCold = false;
+        for (RecallResult.RecallData rd : recallResult.getData()) {
+            if (CollectionUtils.isNotEmpty(rd.getContents())) {
+                for (Content c : rd.getContents()) {
+                    if (utilizePool.equals(c.getContentPoolType())) {
+                        hasUtilize = true;
+                    } else if (coldPool.equals(c.getContentPoolType())) {
+                        hasCold = true;
+                    }
+                    if (hasUtilize && hasCold) {
+                        break;
+                    }
+                }
+            }
+        }
+        boolean needsFallback = !hasUtilize || !hasCold;
+
+        // 回填 experiment_id:需求池内容通过 account_id + sourceId 查 demand_produce_content_relation
+        enrichDemandExperimentId(recallResult, request.getAccountId());
+
+        if (needsFallback) {
+            log.info("recommendDemandPool pool empty utilize:{} cold:{}, running new rank and old recall+rank in parallel. accountName:{}",
+                    hasUtilize, hasCold, request.getAccountName());
+
+            // 并行:新排序 + 老召排
+            CompletableFuture<RankResult> newRankFuture = CompletableFuture.supplyAsync(() -> {
+                long ft1 = System.currentTimeMillis();
+                RankResult result = rankService.rank(convertToRankParam(param, recallResult));
+                long ft2 = System.currentTimeMillis();
+                CostMonitor.logCost("RecommendDemandPool", "NewRank", ft2 - ft1);
+                return result;
+            });
+
+            CompletableFuture<OldRecallRankResult> fallbackFuture = CompletableFuture.supplyAsync(() -> {
+                long ft1 = System.currentTimeMillis();
+                RecommendParam fallbackParam = genRecommendParam(request, SceneConstants.DEFAULT);
+                setStrategy(request, fallbackParam);
+                RecallResult fallbackRecallResult = recallService.recall(convertToRecallParam(fallbackParam));
+                long ft2 = System.currentTimeMillis();
+                CostMonitor.logCost("RecommendDemandPool", "FallbackRecall", ft2 - ft1);
+                if (CollectionUtils.isEmpty(fallbackRecallResult.getData())) {
+                    return null;
+                }
+                RankResult fallbackRankResult = rankService.rank(convertToRankParam(fallbackParam, fallbackRecallResult));
+                long ft3 = System.currentTimeMillis();
+                CostMonitor.logCost("RecommendDemandPool", "FallbackRank", ft3 - ft2);
+                return new OldRecallRankResult(fallbackRecallResult, fallbackRankResult);
+            });
+
+            RankResult newRankResult = newRankFuture.join();
+            OldRecallRankResult fallbackResult = fallbackFuture.join();
+
+            // 合并:非空池用新结果,空池用老结果兜底
+            List<Content> merged = mergeResults(newRankResult.getContents(),
+                    fallbackResult != null ? fallbackResult.rankResult.getContents() : null,
+                    hasUtilize, hasCold, utilizePool, coldPool, param.getPublishNum());
+
+            // 合并 filterContents
+            List<Content> mergedFilterContents = new ArrayList<>();
+            addFilterContents(mergedFilterContents, recallResult);
+            addFilterContents(mergedFilterContents, fallbackResult != null ? fallbackResult.recallResult : null);
+
+            RankResult mergedRankResult = new RankResult(merged);
+            excludeArticleIndex(mergedRankResult, request.getExcludeContentIndex());
+            saveSortLog(param, mergedRankResult);
+
+            // 构建响应时使用合并后的 filterContents
+            RecommendResponse response = buildRecommendResponseWithFilters(mergedRankResult, mergedFilterContents, param.getPublishNum());
+            long t4 = System.currentTimeMillis();
+            log.info("recommendDemandPool merged resultSize:{} total:{}", merged.size(), t4 - start);
+            CostMonitor.logCost("RecommendDemandPool", "Total", t4 - start);
+            return response;
+        }
+
+        // 池子都非空,正常新排序
+        RankResult rankResult = rankService.rank(convertToRankParam(param, recallResult));
+        long t3 = System.currentTimeMillis();
+        CostMonitor.logCost("RecommendDemandPool", "Rank", t3 - t2);
+
+        excludeArticleIndex(rankResult, request.getExcludeContentIndex());
+        saveSortLog(param, rankResult);
+
+        RecommendResponse response = buildRecommendResponse(recallResult, rankResult, param.getPublishNum());
+        long t4 = System.currentTimeMillis();
+        log.info("recommendDemandPool cost param:{} total:{} recall:{} rank:{}",
+                JSONObject.toJSONString(request), t4 - start, t2 - start, t3 - t2);
+        CostMonitor.logCost("RecommendDemandPool", "Total", t4 - start);
+        return response;
+    }
+
+    /**
+     * 合并新排序结果和老排序结果,空池位置用老结果兜底
+     */
+    private List<Content> mergeResults(List<Content> newContents, List<Content> oldContents,
+                                        boolean hasUtilize, boolean hasCold,
+                                        String utilizePool, String coldPool, int size) {
+        List<Content> merged = new ArrayList<>();
+        List<String> usedTitles = new ArrayList<>();
+
+        // 拆分新结果
+        List<Content> newUtilize = new ArrayList<>();
+        List<Content> newCold = new ArrayList<>();
+        if (CollectionUtils.isNotEmpty(newContents)) {
+            for (Content c : newContents) {
+                if (utilizePool.equals(c.getContentPoolType())) {
+                    newUtilize.add(c);
+                } else if (coldPool.equals(c.getContentPoolType())) {
+                    newCold.add(c);
+                }
+            }
+        }
+
+        List<Content> oldList = oldContents != null ? oldContents : new ArrayList<>();
+
+        // 位置 1-2:利用池内容
+        if (hasUtilize) {
+            for (Content c : newUtilize) {
+                if (merged.size() >= 2) break;
+                if (!TitleSimilarCheckUtil.isDuplicateContent(c.getTitle(), usedTitles, TitleSimilarCheckUtil.SIMILARITY_THRESHOLD)) {
+                    merged.add(c);
+                    usedTitles.add(c.getTitle());
+                }
+            }
+        }
+        // 利用池为空,从老结果取兜底
+        if (!hasUtilize) {
+            for (Content c : oldList) {
+                if (merged.size() >= 2) break;
+                if (!TitleSimilarCheckUtil.isDuplicateContent(c.getTitle(), usedTitles, TitleSimilarCheckUtil.SIMILARITY_THRESHOLD)) {
+                    merged.add(c);
+                    usedTitles.add(c.getTitle());
+                }
+            }
+        }
+
+        // 位置 3-8:冷启池内容
+        if (hasCold) {
+            for (Content c : newCold) {
+                if (merged.size() >= size) break;
+                if (!TitleSimilarCheckUtil.isDuplicateContent(c.getTitle(), usedTitles, TitleSimilarCheckUtil.SIMILARITY_THRESHOLD)) {
+                    merged.add(c);
+                    usedTitles.add(c.getTitle());
+                }
+            }
+        }
+        // 冷启池为空,从老结果取兜底
+        if (!hasCold) {
+            for (Content c : oldList) {
+                if (merged.size() >= size) break;
+                if (!TitleSimilarCheckUtil.isDuplicateContent(c.getTitle(), usedTitles, TitleSimilarCheckUtil.SIMILARITY_THRESHOLD)) {
+                    // 跳过已经被利用池使用的
+                    boolean alreadyUsed = false;
+                    for (Content existing : merged) {
+                        if (Objects.equals(existing.getId(), c.getId())) {
+                            alreadyUsed = true;
+                            break;
+                        }
+                    }
+                    if (!alreadyUsed) {
+                        if (!TitleSimilarCheckUtil.isDuplicateContent(c.getTitle(), usedTitles, TitleSimilarCheckUtil.SIMILARITY_THRESHOLD)) {
+                            merged.add(c);
+                            usedTitles.add(c.getTitle());
+                        }
+                    }
+                }
+            }
+        }
+
+        return merged;
+    }
+
+    private void addFilterContents(List<Content> target, RecallResult recallResult) {
+        if (recallResult == null || CollectionUtils.isEmpty(recallResult.getData())) {
+            return;
+        }
+        for (RecallResult.RecallData data : recallResult.getData()) {
+            if (CollectionUtils.isNotEmpty(data.getFilterContents())) {
+                target.addAll(data.getFilterContents());
+            }
+        }
+    }
+
+    private RecommendResponse buildRecommendResponseWithFilters(RankResult rankResult,
+                                                                  List<Content> filterContents, Integer publishNum) {
+        List<Content> contentList = rankResult.getContents();
+        List<ArticleSortResponseDataItem> rankList = new ArrayList<>();
+        List<ArticleSortResponseDataItem> filterContentList = new ArrayList<>();
+        if (CollectionUtils.isNotEmpty(filterContents)) {
+            for (Content filterContent : filterContents) {
+                ArticleSortResponseDataItem item = new ArticleSortResponseDataItem();
+                BeanUtils.copyProperties(filterContent, item);
+                filterContentList.add(item);
+            }
+        }
+        ArticleSortResponseData data = new ArticleSortResponseData();
+        if (CollectionUtils.isNotEmpty(contentList)) {
+            for (Content content : contentList) {
+                ArticleSortResponseDataItem item = new ArticleSortResponseDataItem();
+                BeanUtils.copyProperties(content, item);
+                rankList.add(item);
+            }
+        }
+        data.setRank_list(rankList.subList(0, Math.min(rankList.size(), publishNum)));
+        data.setFilter_list(filterContentList);
+
+        CompletableFuture.runAsync(() -> updateWaitingContentFilter(filterContentList));
+
+        RecommendResponse response = new RecommendResponse();
+        response.setCode(0);
+        response.setMsg("success");
+        response.setData(data);
+        return response;
+    }
+
+    private static class OldRecallRankResult {
+        final RecallResult recallResult;
+        final RankResult rankResult;
+
+        OldRecallRankResult(RecallResult recallResult, RankResult rankResult) {
+            this.recallResult = recallResult;
+            this.rankResult = rankResult;
+        }
+    }
+
+    /**
+     * 从 demand_search_article_relation 表回填 experiment_id 到召回结果
+     */
+    /**
+     * 回填 experiment_id 到召回结果
+     * - accountDemandPoolLevelCold / accountDemandPoolLevelUtilize → demand_produce_content_relation (by account_id + sourceId)
+     * - 其他内容 → demand_search_article_relation (by channel_content_id)
+     */
+    private void enrichDemandExperimentId(RecallResult recallResult, String accountId) {
+        if (recallResult == null || CollectionUtils.isEmpty(recallResult.getData())) {
+            return;
+        }
+        String coldPool = ContentPoolEnum.accountDemandPoolLevelCold.getContentPool();
+        String utilizePool = ContentPoolEnum.accountDemandPoolLevelUtilize.getContentPool();
+        Set<String> demandPoolSourceIds = new HashSet<>();
+        Set<String> otherChannelContentIds = new HashSet<>();
+
+        for (RecallResult.RecallData rd : recallResult.getData()) {
+            if (CollectionUtils.isNotEmpty(rd.getContents())) {
+                for (Content c : rd.getContents()) {
+                    if (coldPool.equals(c.getContentPoolType()) || utilizePool.equals(c.getContentPoolType())) {
+                        if (StringUtils.hasText(c.getSourceId())) {
+                            demandPoolSourceIds.add(c.getSourceId());
+                        }
+                    } else {
+                        if (StringUtils.hasText(c.getCrawlerChannelContentId())) {
+                            otherChannelContentIds.add(c.getCrawlerChannelContentId());
+                        }
+                    }
+                }
+            }
+        }
+
+        Map<String, String> contentIdToExperimentId = new HashMap<>();
+
+        // 需求池:通过 account_id + sourceId(=content_id) 查 demand_produce_content_relation
+        if (!demandPoolSourceIds.isEmpty() && StringUtils.hasText(accountId)) {
+            List<DemandSearchArticleRelationDTO> mappings = longArticleBaseMapper
+                    .getDemandExperimentIdByContentIds(new ArrayList<>(demandPoolSourceIds), accountId);
+            if (CollectionUtils.isNotEmpty(mappings)) {
+                for (DemandSearchArticleRelationDTO m : mappings) {
+                    if (StringUtils.hasText(m.getContentId()) && StringUtils.hasText(m.getExperimentId())) {
+                        contentIdToExperimentId.put(m.getContentId(), m.getExperimentId());
+                    }
+                }
+            }
+        }
+
+        // 其他内容:查 demand_search_article_relation
+        if (!otherChannelContentIds.isEmpty()) {
+            List<DemandSearchArticleRelationDTO> mappings = longArticleBaseMapper
+                    .getDemandExperimentIdByChannelContentIds(new ArrayList<>(otherChannelContentIds));
+            if (CollectionUtils.isNotEmpty(mappings)) {
+                for (DemandSearchArticleRelationDTO m : mappings) {
+                    if (StringUtils.hasText(m.getChannelContentId()) && StringUtils.hasText(m.getExperimentId())) {
+                        contentIdToExperimentId.put(m.getChannelContentId(), m.getExperimentId());
+                    }
+                }
+            }
+        }
+
+        if (contentIdToExperimentId.isEmpty()) {
+            return;
+        }
+
+        // 回填到 Content:需求池按 sourceId 匹配,其他按 crawlerChannelContentId
+        for (RecallResult.RecallData rd : recallResult.getData()) {
+            if (CollectionUtils.isNotEmpty(rd.getContents())) {
+                for (Content c : rd.getContents()) {
+                    if (coldPool.equals(c.getContentPoolType()) || utilizePool.equals(c.getContentPoolType())) {
+                        String expId = contentIdToExperimentId.get(c.getSourceId());
+                        if (expId != null) {
+                            c.setExperimentId(expId);
+                        }
+                    } else {
+                        String expId = contentIdToExperimentId.get(c.getCrawlerChannelContentId());
+                        if (expId != null) {
+                            c.setExperimentId(expId);
+                        }
+                    }
+                }
+            }
+        }
+        log.info("enrichDemandExperimentId enriched {} experimentIds (demandPool={}, other={})",
+                contentIdToExperimentId.size(), demandPoolSourceIds.size(), otherChannelContentIds.size());
+    }
+
     private RecommendResponse buildRecommendResponse(RecallResult recallResult, RankResult rankResult, Integer publishNum) {
         List<Content> contentList = rankResult.getContents();
         List<ArticleSortResponseDataItem> rankList = new ArrayList<>();
@@ -498,6 +839,306 @@ public class RecommendService {
         }
     }
 
+    /**
+     * 跨账号冷启池统一排序分配。
+     * 收集 plan 下所有账号的冷启内容,全局排序后按位置阅读均值门控分配到各账号冷启位(3-8)。
+     */
+    public PlanColdStartSortResponse planColdStartSort(PlanColdStartSortRequest request) {
+        long start = System.currentTimeMillis();
+
+        if (request == null || CollectionUtils.isEmpty(request.getAccounts())) {
+            return PlanColdStartSortResponse.fail("accounts不能为空");
+        }
+        if (!StringUtils.hasText(request.getPlanId())) {
+            return PlanColdStartSortResponse.fail("planId不能为空");
+        }
+
+        String coldPool = ContentPoolEnum.accountDemandPoolLevelCold.getContentPool();
+        List<PlanColdStartSortRequest.AccountInfo> accounts = request.getAccounts();
+
+        // Phase 1 & 2: 并行 per-account recall + rank (只处理冷启内容)
+        List<CompletableFuture<AccountColdRankResult>> futures = new ArrayList<>();
+        for (PlanColdStartSortRequest.AccountInfo account : accounts) {
+            futures.add(CompletableFuture.supplyAsync(() -> rankAccountColdContent(request.getPlanId(), account, coldPool)));
+        }
+
+        List<AccountColdRankResult> perAccountResults = futures.stream()
+                .map(CompletableFuture::join)
+                .collect(Collectors.toList());
+
+        // Phase 3: 合并所有账号冷启内容,全局按 score 降序排序
+        List<GlobalColdItem> globalColdList = new ArrayList<>();
+        for (AccountColdRankResult ar : perAccountResults) {
+            if (CollectionUtils.isNotEmpty(ar.coldContents)) {
+                for (Content c : ar.coldContents) {
+                    globalColdList.add(new GlobalColdItem(c, ar.accountId, ar.accountName, ar.ghId));
+                }
+            }
+        }
+        globalColdList.sort((a, b) -> Double.compare(b.content.getScore(), a.content.getScore()));
+        log.info("planColdStartSort planId:{} globalColdList size:{}", request.getPlanId(), globalColdList.size());
+
+        // Phase 4: 批量查阅读均值 + 位置分配
+        // 收集所有 ghId
+        Set<String> ghIds = accounts.stream()
+                .map(PlanColdStartSortRequest.AccountInfo::getGhId)
+                .filter(StringUtils::hasText)
+                .collect(Collectors.toSet());
+        List<AccountAvgInfo> allAvgInfo = accountAvgInfoRepository.getAllByGhIdInAndStatusEquals(ghIds, 1);
+        Map<String, List<AccountAvgInfo>> avgByGhId = allAvgInfo.stream()
+                .collect(Collectors.groupingBy(AccountAvgInfo::getGhId));
+
+        // 初始化每个账号的冷启位 tracker: accountId -> nextUnfilledPosition (3-8)
+        Map<String, Integer> nextPosition = new LinkedHashMap<>();
+        Map<String, PlanColdStartSortResponse.AccountAllocation> allocationMap = new LinkedHashMap<>();
+        for (PlanColdStartSortRequest.AccountInfo account : accounts) {
+            nextPosition.put(account.getAccountId(), 3);
+            PlanColdStartSortResponse.AccountAllocation alloc = new PlanColdStartSortResponse.AccountAllocation();
+            alloc.setAccountId(account.getAccountId());
+            alloc.setAccountName(account.getAccountName());
+            alloc.setGhId(account.getGhId());
+            alloc.setPositions(new ArrayList<>());
+            allocationMap.put(account.getAccountId(), alloc);
+        }
+
+        int allocatedCount = 0;
+        int skippedCount = 0;
+        List<String> allocatedSourceIds = new ArrayList<>();
+        int totalColdPositions = accounts.size() * 6; // positions 3-8 per account
+        // sourceId → Content 映射,用于后续写入 publish_sort_log
+        Map<String, Content> sourceIdToContent = new HashMap<>();
+
+        for (GlobalColdItem item : globalColdList) {
+            if (allocatedCount >= totalColdPositions) break;
+
+            String accId = item.accountId;
+            String ghId = item.ghId;
+            Integer nextPos = nextPosition.get(accId);
+            if (nextPos == null || nextPos > 8) {
+                continue; // 该账号冷启位已满
+            }
+
+            // 查找该账号下一个阅读均值 <= threshold 的位置
+            Integer assignedPos = null;
+            for (int pos = nextPos; pos <= 8; pos++) {
+                double avgRead = accountIndexAvgViewCountService.getAvgReadCountByDB(
+                        avgByGhId.getOrDefault(ghId, Collections.emptyList()), ghId, pos);
+                if (avgRead <= coldStartReadingAvgThreshold) {
+                    assignedPos = pos;
+                    break;
+                }
+            }
+
+            if (assignedPos == null) {
+                // 该账号所有剩余位置阅读均值都超过阈值
+                skippedCount++;
+                continue;
+            }
+
+            // 分配
+            PlanColdStartSortResponse.PositionAllocation pa = new PlanColdStartSortResponse.PositionAllocation();
+            pa.setPositionIndex(assignedPos);
+            pa.setSourceId(item.content.getSourceId());
+            pa.setPublishContentId(item.content.getId());
+            pa.setSourceType(item.content.getSourceType());
+            pa.setTitle(item.content.getTitle());
+            pa.setScore(item.content.getScore());
+            pa.setContentPoolType(item.content.getContentPoolType());
+            pa.setExperimentId(item.content.getExperimentId());
+            pa.setSkipped(false);
+
+            double assignedAvgRead = accountIndexAvgViewCountService.getAvgReadCountByDB(
+                    avgByGhId.getOrDefault(ghId, Collections.emptyList()), ghId, assignedPos);
+            pa.setPositionAvgRead(assignedAvgRead);
+
+            allocationMap.get(accId).getPositions().add(pa);
+            allocatedSourceIds.add(item.content.getSourceId());
+            sourceIdToContent.put(item.content.getSourceId(), item.content);
+            nextPosition.put(accId, assignedPos + 1);
+            allocatedCount++;
+        }
+
+        // Phase 4.5: 保存排序结果到 publish_sort_log(供后续数据统计晋级流程使用)
+        savePlanColdStartSortLog(accounts, allocationMap, sourceIdToContent, avgByGhId);
+
+        // Phase 4.6: 清理 publish_content_gzh_waiting 中已分配的 sourceId(标记 status=0)
+        if (!allocatedSourceIds.isEmpty()) {
+            long updateTimestamp = System.currentTimeMillis();
+            for (List<String> partition : Lists.partition(allocatedSourceIds, 2000)) {
+                longArticleBaseMapper.batchUpdateContentStatusBySourceIds(partition, 0, updateTimestamp);
+            }
+            log.info("planColdStartSort cleaned {} sourceIds from publish_content_gzh_waiting", allocatedSourceIds.size());
+        }
+
+        // Phase 5: 构建响应
+        List<PlanColdStartSortResponse.AccountAllocation> data = new ArrayList<>(allocationMap.values());
+        long totalCost = System.currentTimeMillis() - start;
+        log.info("planColdStartSort planId:{} accounts:{} allocated:{} skipped:{} totalColdContents:{} cost:{}ms",
+                request.getPlanId(), accounts.size(), allocatedCount, skippedCount, globalColdList.size(), totalCost);
+
+        return PlanColdStartSortResponse.success(data, allocatedSourceIds);
+    }
+
+    /**
+     * 对单个账号做冷启内容 recall + rank,返回已打分排序的冷启内容列表
+     */
+    private AccountColdRankResult rankAccountColdContent(String planId, PlanColdStartSortRequest.AccountInfo account, String coldPool) {
+        RecommendParam param = new RecommendParam();
+        param.setAccountId(account.getAccountId());
+        param.setAccountName(account.getAccountName());
+        param.setGhId(account.getGhId());
+        param.setPlanId(planId);
+        param.setScene(DEMAND_POOL);
+        param.setStrategy(RankStrategyEnum.DEMAND_POOL_STRATEGY.getStrategy());
+        // 足够大的 publishNum 以确保所有冷启内容都被 rank
+        param.setPublishNum(6);
+
+        // recall
+        RecallResult recallResult = recallService.recall(convertToRecallParam(param));
+
+        // 只取冷启池内容
+        List<Content> coldContents = new ArrayList<>();
+        if (recallResult != null && CollectionUtils.isNotEmpty(recallResult.getData())) {
+            for (RecallResult.RecallData rd : recallResult.getData()) {
+                if (CollectionUtils.isNotEmpty(rd.getContents())) {
+                    for (Content c : rd.getContents()) {
+                        if (coldPool.equals(c.getContentPoolType())) {
+                            coldContents.add(c);
+                        }
+                    }
+                }
+            }
+        }
+
+        if (coldContents.isEmpty()) {
+            return new AccountColdRankResult(account.getAccountId(), account.getAccountName(), account.getGhId(),
+                    Collections.emptyList());
+        }
+
+        // rank 冷启内容
+        RankParam rankParam = new RankParam();
+        rankParam.setContents(coldContents);
+        rankParam.setBackup(Collections.emptyList());
+        rankParam.setStrategy(param.getStrategy());
+        rankParam.setGhId(param.getGhId());
+        rankParam.setAccountId(param.getAccountId());
+        rankParam.setAccountName(param.getAccountName());
+        rankParam.setSize(coldContents.size());
+        rankParam.setScene(param.getScene());
+        rankParam.setPlanId(param.getPlanId());
+
+        RankResult rankResult = rankService.rank(rankParam);
+
+        return new AccountColdRankResult(account.getAccountId(), account.getAccountName(), account.getGhId(),
+                rankResult != null ? rankResult.getContents() : Collections.emptyList());
+    }
+
+    private static class AccountColdRankResult {
+        final String accountId;
+        final String accountName;
+        final String ghId;
+        final List<Content> coldContents;
+
+        AccountColdRankResult(String accountId, String accountName, String ghId, List<Content> coldContents) {
+            this.accountId = accountId;
+            this.accountName = accountName;
+            this.ghId = ghId;
+            this.coldContents = coldContents;
+        }
+    }
+
+    private static class GlobalColdItem {
+        final Content content;
+        final String accountId;
+        final String accountName;
+        final String ghId;
+
+        GlobalColdItem(Content content, String accountId, String accountName, String ghId) {
+            this.content = content;
+            this.accountId = accountId;
+            this.accountName = accountName;
+            this.ghId = ghId;
+        }
+    }
+
+    /**
+     * 将跨账号冷启池分配结果写入 publish_sort_log,
+     * 确保后续数据统计晋级流程可以正确读取排序结果。
+     * 写入逻辑与 saveSortLog 保持一致:按 ghId + dateStr 去重 title,记录 position、score、strategy 等。
+     */
+    private void savePlanColdStartSortLog(
+            List<PlanColdStartSortRequest.AccountInfo> accounts,
+            Map<String, PlanColdStartSortResponse.AccountAllocation> allocationMap,
+            Map<String, Content> sourceIdToContent,
+            Map<String, List<AccountAvgInfo>> avgByGhId) {
+        if (!"prod".equals(env)) {
+            return;
+        }
+        try {
+            String dateStr = DateUtils.getCurrentDateStr("yyyyMMdd");
+            String strategy = RankStrategyEnum.DEMAND_POOL_STRATEGY.getStrategy();
+            List<PublishSortLog> publishSortLogSaveList = new ArrayList<>();
+
+            for (PlanColdStartSortRequest.AccountInfo account : accounts) {
+                PlanColdStartSortResponse.AccountAllocation alloc = allocationMap.get(account.getAccountId());
+                if (alloc == null || CollectionUtils.isEmpty(alloc.getPositions())) {
+                    continue;
+                }
+
+                // 获取该账号当天已有记录,按 title 去重
+                List<PublishSortLog> hisSortLog = publishSortLogRepository.findByDateStrAndGhId(dateStr, account.getGhId());
+                Set<String> hisSortTitles = hisSortLog != null
+                        ? hisSortLog.stream().map(PublishSortLog::getTitle).collect(Collectors.toSet())
+                        : Collections.emptySet();
+
+                List<AccountAvgInfo> avgInfoList = avgByGhId.getOrDefault(account.getGhId(), Collections.emptyList());
+
+                for (PlanColdStartSortResponse.PositionAllocation pa : alloc.getPositions()) {
+                    if (pa.isSkipped() || !StringUtils.hasText(pa.getSourceId())) {
+                        continue;
+                    }
+                    // 同一天同一账号不重复记录相同 title
+                    if (hisSortTitles.contains(pa.getTitle())) {
+                        continue;
+                    }
+
+                    Content content = sourceIdToContent.get(pa.getSourceId());
+                    if (content == null) {
+                        continue;
+                    }
+
+                    PublishSortLog sortLog = new PublishSortLog();
+                    sortLog.setDateStr(dateStr);
+                    sortLog.setGhId(account.getGhId());
+                    sortLog.setAccountName(account.getAccountName());
+                    sortLog.setPublishContentId(content.getId());
+                    sortLog.setCrawlerChannelContentId(content.getCrawlerChannelContentId());
+                    sortLog.setSourceType(content.getSourceType());
+                    sortLog.setSourceId(content.getSourceId());
+                    sortLog.setTitle(content.getTitle());
+                    sortLog.setIndex(pa.getPositionIndex());
+                    sortLog.setIndexAvgCount(accountIndexAvgViewCountService.getAvgReadCountByDB(
+                            avgInfoList, account.getGhId(), pa.getPositionIndex()));
+                    if (CollectionUtils.isNotEmpty(content.getCategory())) {
+                        sortLog.setCategory(JSONObject.toJSONString(content.getCategory()));
+                    }
+                    sortLog.setStrategy(strategy);
+                    sortLog.setScore(String.valueOf(content.getScore()));
+                    sortLog.setScoreMap(JSONObject.toJSONString(content.getScoreMap()));
+                    sortLog.setCreateTimestamp(System.currentTimeMillis());
+                    publishSortLogSaveList.add(sortLog);
+                }
+            }
+
+            if (CollectionUtils.isNotEmpty(publishSortLogSaveList)) {
+                publishSortLogRepository.saveAll(publishSortLogSaveList);
+                log.info("savePlanColdStartSortLog saved {} records to publish_sort_log", publishSortLogSaveList.size());
+            }
+        } catch (Exception e) {
+            log.error("savePlanColdStartSortLog error", e);
+        }
+    }
+
     /**
      * 删除指定计划与发布账号下的 publish_content_gzh_waiting 数据,
      * 并清理对应的 ContentPreFilterJob redis 缓存。

+ 27 - 0
long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/service/recommend/filter/FilterService.java

@@ -26,6 +26,7 @@ import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 
+import static com.tzld.longarticle.recommend.server.common.constant.SceneConstants.DEMAND_POOL;
 import static com.tzld.longarticle.recommend.server.common.constant.SceneConstants.FWH_COLD_START;
 
 @Slf4j
@@ -132,6 +133,17 @@ public class FilterService {
 
     private List<FilterStrategy> getAllStrategies(FilterParam param) {
         List<FilterStrategy> strategies = new ArrayList<>();
+        if (param.getScene().equals(DEMAND_POOL)) {
+            strategies.add(ServiceBeanFactory.getBean(BadStrategy.class));
+            strategies.add(ServiceBeanFactory.getBean(SensitiveStrategy.class));
+            strategies.add(ServiceBeanFactory.getBean(DeDuplicationStrategy.class));
+            strategies.add(ServiceBeanFactory.getBean(KeywordStrategy.class));
+            strategies.add(ServiceBeanFactory.getBean(DemandPoolPromotionStrategy.class));
+            strategies.add(ServiceBeanFactory.getBean(TodayPublishStrategy.class));
+            strategies.add(ServiceBeanFactory.getBean(CategoryFilterStrategy.class));
+            strategies.add(ServiceBeanFactory.getBean(HistoryTitleStrategy.class));
+            return strategies;
+        }
         strategies.add(ServiceBeanFactory.getBean(BadStrategy.class));
         strategies.add(ServiceBeanFactory.getBean(SensitiveStrategy.class));
         strategies.add(ServiceBeanFactory.getBean(DeDuplicationStrategy.class));
@@ -157,6 +169,16 @@ public class FilterService {
 
     private List<FilterStrategy> getPreFilterStrategies(FilterParam param) {
         List<FilterStrategy> strategies = new ArrayList<>();
+        if (param.getScene().equals(DEMAND_POOL)) {
+            strategies.add(ServiceBeanFactory.getBean(BadStrategy.class));
+            strategies.add(ServiceBeanFactory.getBean(SensitiveStrategy.class));
+            strategies.add(ServiceBeanFactory.getBean(DeDuplicationStrategy.class));
+            strategies.add(ServiceBeanFactory.getBean(KeywordStrategy.class));
+            strategies.add(ServiceBeanFactory.getBean(DemandPoolPromotionStrategy.class));
+            strategies.add(ServiceBeanFactory.getBean(CategoryFilterStrategy.class));
+            strategies.add(ServiceBeanFactory.getBean(HistoryTitleStrategy.class));
+            return strategies;
+        }
         strategies.add(ServiceBeanFactory.getBean(BadStrategy.class));
         strategies.add(ServiceBeanFactory.getBean(SensitiveStrategy.class));
         strategies.add(ServiceBeanFactory.getBean(DeDuplicationStrategy.class));
@@ -179,6 +201,11 @@ public class FilterService {
 
     private List<FilterStrategy> getUnPreFilterStrategies(FilterParam param) {
         List<FilterStrategy> strategies = new ArrayList<>();
+        if (param.getScene().equals(DEMAND_POOL)) {
+            strategies.add(ServiceBeanFactory.getBean(TodayPublishStrategy.class));
+            strategies.add(ServiceBeanFactory.getBean(CategoryFilterStrategy.class));
+            return strategies;
+        }
         strategies.add(ServiceBeanFactory.getBean(TodayPublishStrategy.class));
         strategies.add(ServiceBeanFactory.getBean(CategoryFilterStrategy.class));
         if (!param.getScene().equals(FWH_COLD_START)

+ 62 - 0
long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/service/recommend/filter/strategy/DemandPoolPromotionStrategy.java

@@ -0,0 +1,62 @@
+package com.tzld.longarticle.recommend.server.service.recommend.filter.strategy;
+
+import com.tzld.longarticle.recommend.server.model.dto.Content;
+import com.tzld.longarticle.recommend.server.model.entity.longArticle.DemandPoolPromotionSource;
+import com.tzld.longarticle.recommend.server.repository.longArticle.DemandPoolPromotionSourceRepository;
+import com.tzld.longarticle.recommend.server.service.recommend.filter.FilterParam;
+import com.tzld.longarticle.recommend.server.service.recommend.filter.FilterResult;
+import com.tzld.longarticle.recommend.server.service.recommend.filter.FilterStrategy;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * 需求匹配内容池晋级过滤
+ * 过滤已从冷启池晋级到利用池的内容(独立晋级表 demand_pool_promotion_source)
+ *
+ * @author dyp
+ */
+@Component
+@Slf4j
+public class DemandPoolPromotionStrategy implements FilterStrategy {
+
+    @Autowired
+    DemandPoolPromotionSourceRepository repository;
+
+    @Override
+    public FilterResult filter(FilterParam param) {
+        FilterResult filterResult = new FilterResult();
+        List<String> result = new ArrayList<>();
+        List<Content> contents = param.getContents();
+        List<Content> filterContents = new ArrayList<>();
+
+        List<DemandPoolPromotionSource> promotionSourceList = repository.getByStatusAndDeleted(1, 0);
+        Map<String, DemandPoolPromotionSource> promotionSourceMap = promotionSourceList.stream()
+                .collect(Collectors.toMap(DemandPoolPromotionSource::getChannelContentId, a -> a, (a, b) -> a));
+
+        for (Content content : contents) {
+            if (promotionSourceMap.containsKey(content.getCrawlerChannelContentId())) {
+                DemandPoolPromotionSource promotionSource = promotionSourceMap.get(content.getCrawlerChannelContentId());
+                if (!promotionSource.getLevel().equals(content.getContentPoolType())) {
+                    content.setFilterReason("内容已晋级过滤");
+                    filterContents.add(content);
+                    continue;
+                }
+            }
+            if (!StringUtils.hasText(content.getFilterReason())) {
+                result.add(content.getId());
+            }
+        }
+
+        filterResult.setContentIds(result);
+        filterResult.setFilterContent(filterContents);
+        return filterResult;
+    }
+
+}

+ 4 - 0
long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/service/recommend/rank/RankService.java

@@ -16,6 +16,7 @@ import com.tzld.longarticle.recommend.server.model.dto.Content;
 import com.tzld.longarticle.recommend.server.model.entity.aigc.PublishContent;
 import com.tzld.longarticle.recommend.server.repository.aigc.PublishContentRepository;
 import com.tzld.longarticle.recommend.server.service.ServiceBeanFactory;
+import com.tzld.longarticle.recommend.server.service.recommend.rank.strategy.DemandPoolRankStrategy;
 import com.tzld.longarticle.recommend.server.service.recommend.rank.strategy.FwhColdStartRankStrategy;
 import com.tzld.longarticle.recommend.server.service.recommend.score.AccountIndexReplacePoolConfig;
 import com.tzld.longarticle.recommend.server.util.TitleSimilarCheckUtil;
@@ -32,6 +33,7 @@ import java.util.Map;
 import java.util.Objects;
 import java.util.stream.Collectors;
 
+import static com.tzld.longarticle.recommend.server.common.constant.SceneConstants.DEMAND_POOL;
 import static com.tzld.longarticle.recommend.server.common.constant.SceneConstants.FWH_COLD_START;
 
 /**
@@ -101,6 +103,8 @@ public class RankService implements InitializingBean {
         switch (param.getScene()) {
             case FWH_COLD_START:
                 return ServiceBeanFactory.getBean(FwhColdStartRankStrategy.class);
+            case DEMAND_POOL:
+                return ServiceBeanFactory.getBean(DemandPoolRankStrategy.class);
             default:
                 RankStrategyEnum rankStrategyEnum = RankStrategyEnum.from(param.getStrategy());
                 return (RankStrategy) ServiceBeanFactory.getBeanByName(rankStrategyEnum.getTaskExecutorName());

+ 128 - 0
long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/service/recommend/rank/strategy/DemandPoolRankStrategy.java

@@ -0,0 +1,128 @@
+package com.tzld.longarticle.recommend.server.service.recommend.rank.strategy;
+
+import com.tzld.longarticle.recommend.server.common.enums.recommend.ContentPoolEnum;
+import com.tzld.longarticle.recommend.server.common.enums.recommend.ScoreStrategyEnum;
+import com.tzld.longarticle.recommend.server.model.dto.Content;
+import com.tzld.longarticle.recommend.server.service.recommend.config.StrategyIndexScoreWeightService;
+import com.tzld.longarticle.recommend.server.service.recommend.rank.*;
+import com.tzld.longarticle.recommend.server.service.recommend.score.ScoreResult;
+import com.tzld.longarticle.recommend.server.service.recommend.score.ScoreService;
+import com.tzld.longarticle.recommend.server.util.CommonCollectionUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections4.CollectionUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.*;
+
+/**
+ * 需求匹配内容池排序策略
+ * <p>
+ * 内容池划分:
+ * - 利用池 (accountDemandPoolLevelUtilize): 头次,发 1-2 位
+ * - 冷启池 (accountDemandPoolLevelCold): 冷启,发 3-8 位
+ * <p>
+ * 排序使用历史表现阅读分 + 品类分,安全分降权
+ *
+ * @author dyp
+ */
+@Service
+@Slf4j
+public class DemandPoolRankStrategy implements RankStrategy {
+
+    @Autowired
+    private ScoreService scoreService;
+
+    @Autowired
+    private StrategyIndexScoreWeightService weightService;
+
+    @Override
+    public RankResult rank(RankParam param) {
+
+        ScoreResult scoreResult = scoreService.score(RankStrategy.convertToScoreParam(param));
+        Map<String, Map<String, Double>> scoreMap = scoreResult.getScoreMap();
+
+        String utilizePool = ContentPoolEnum.accountDemandPoolLevelUtilize.getContentPool();
+        String coldPool = ContentPoolEnum.accountDemandPoolLevelCold.getContentPool();
+
+        // 按内容池划分
+        List<RankItem> utilizeItems = new ArrayList<>();
+        List<RankItem> coldItems = new ArrayList<>();
+
+        for (Content c : param.getContents()) {
+            RankItem item = new RankItem();
+            c.setHisPublishArticleList(null);
+            item.setContent(c);
+            c.setScoreMap(scoreMap.get(c.getId()));
+            item.setScoreMap(scoreMap.get(c.getId()));
+
+            String contentPoolType = c.getContentPoolType();
+            double score;
+
+            if (utilizePool.equals(contentPoolType)) {
+                // 利用池(头次):加权分数
+                int index = 1;
+                score = item.getScore(ScoreStrategyEnum.VIEW_COUNT.value())
+                        * weightService.getWeight(param.getStrategy(), param.getGhId(), index,
+                        ScoreStrategyEnum.VIEW_COUNT.value())
+                        + item.getScore(ScoreStrategyEnum.VIEW_COUNT_RATE.value())
+                        * weightService.getWeight(param.getStrategy(), param.getGhId(), index,
+                        ScoreStrategyEnum.VIEW_COUNT_RATE.value())
+                        + item.getScore(ScoreStrategyEnum.CATEGORY.value())
+                        * weightService.getWeight(param.getStrategy(), param.getGhId(), index,
+                        ScoreStrategyEnum.CATEGORY.value())
+                        + item.getScore(ScoreStrategyEnum.FLOW_CTL_DECREASE.value());
+                item.setScore(score);
+                utilizeItems.add(item);
+            } else if (coldPool.equals(contentPoolType)) {
+                // 冷启池(3-8位):加权分数
+                int index = 3;
+                score = item.getScore(ScoreStrategyEnum.VIEW_COUNT.value())
+                        * weightService.getWeight(param.getStrategy(), param.getGhId(), index,
+                        ScoreStrategyEnum.VIEW_COUNT.value())
+                        + item.getScore(ScoreStrategyEnum.VIEW_COUNT_RATE.value())
+                        * weightService.getWeight(param.getStrategy(), param.getGhId(), index,
+                        ScoreStrategyEnum.VIEW_COUNT_RATE.value())
+                        + item.getScore(ScoreStrategyEnum.CATEGORY.value())
+                        * weightService.getWeight(param.getStrategy(), param.getGhId(), index,
+                        ScoreStrategyEnum.CATEGORY.value())
+                        + item.getScore(ScoreStrategyEnum.FLOW_CTL_DECREASE.value());
+                item.setScore(score);
+                coldItems.add(item);
+            }
+        }
+
+        // 安全分降权
+        RankService.safeScoreDecrease(utilizeItems);
+        RankService.safeScoreDecrease(coldItems);
+
+        // 各池按分数降序排序
+        utilizeItems.sort((o1, o2) -> -Double.compare(o1.getScore(), o2.getScore()));
+        coldItems.sort((o1, o2) -> -Double.compare(o1.getScore(), o2.getScore()));
+
+        // 相似去重
+        List<Content> utilizeContents = CommonCollectionUtils.toList(utilizeItems, RankItem::getContent);
+        List<Content> coldContents = CommonCollectionUtils.toList(coldItems, RankItem::getContent);
+
+        // 异步保存排序内容分数
+        List<RankItem> allItems = new ArrayList<>();
+        allItems.addAll(utilizeItems);
+        allItems.addAll(coldItems);
+        RankStrategy.asyncSaveRankContentScore(param, allItems);
+
+        // 组装结果:利用池取前2发头次,冷启池取前6发3-8位
+        List<Content> result = new ArrayList<>();
+        int utilizeCount = Math.min(utilizeContents.size(), 2);
+        result.addAll(utilizeContents.subList(0, utilizeCount));
+
+        int remaining = param.getSize() - result.size();
+        if (remaining > 0 && CollectionUtils.isNotEmpty(coldContents)) {
+            int coldCount = Math.min(coldContents.size(), remaining);
+            result.addAll(coldContents.subList(0, coldCount));
+        }
+
+        RankResult rankResult = new RankResult(result);
+        return rankResult;
+    }
+
+}

+ 9 - 0
long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/service/recommend/score/ScoreService.java

@@ -105,6 +105,15 @@ public class ScoreService implements ApplicationContextAware {
         if (StringUtils.equals(param.getStrategy(), RankStrategyEnum.RANDOM_STRATEGY.getStrategy())) {
             return strategies;
         }
+        if (StringUtils.equals(param.getStrategy(), RankStrategyEnum.DEMAND_POOL_STRATEGY.getStrategy())) {
+            strategies.add(strategyMap.get(ScoreStrategyEnum.VIEW_COUNT.value()));
+            strategies.add(strategyMap.get(ScoreStrategyEnum.VIEW_COUNT_RATE.value()));
+            strategies.add(strategyMap.get(ScoreStrategyEnum.VIEW_COUNT_RATE_V2.value()));
+            strategies.add(strategyMap.get(ScoreStrategyEnum.CATEGORY.value()));
+            strategies.add(strategyMap.get(ScoreStrategyEnum.ACCOUNT_USER_CATEGORY.value()));
+            strategies.add(strategyMap.get(ScoreStrategyEnum.FLOW_CTL_DECREASE.value()));
+            return strategies;
+        }
         strategies.add(strategyMap.get(ScoreStrategyEnum.CRAWLER_DAYS_DECREASE_STRATEGY.value()));
         strategies.add(strategyMap.get(ScoreStrategyEnum.CRAWLER_DAYS_DECREASE_V2_STRATEGY.value()));
         if (!similarityStopStrategies.contains(param.getStrategy())

+ 13 - 3
long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/web/XxlJobController.java

@@ -72,14 +72,24 @@ public class XxlJobController {
         service.articlePromotion(null);
     }
 
+    @GetMapping("/articlePromotionTraceability")
+    public void articlePromotionTraceability(String channelContentId) {
+        service.articlePromotionTraceability(channelContentId);
+    }
+
     @GetMapping("/articleVideoPoolPromotion")
     public void articleVideoPoolPromotion() {
         service.articleVideoPoolPromotion(null);
     }
 
-    @GetMapping("/articlePromotionTraceability")
-    public void articlePromotionTraceability(String channelContentId) {
-        service.articlePromotionTraceability(channelContentId);
+    @GetMapping("/demandPoolPromotion")
+    public void demandPoolPromotion() {
+        service.demandPoolPromotion(null);
+    }
+
+    @GetMapping("/demandPoolPromotionTraceability")
+    public void demandPoolPromotionTraceability(String channelContentId) {
+        service.demandPoolPromotionTraceability(channelContentId);
     }
 
     @GetMapping("/articleCategoryJob")

+ 37 - 0
long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/web/recommend/FilterContentByAccountController.java

@@ -0,0 +1,37 @@
+package com.tzld.longarticle.recommend.server.web.recommend;
+
+import com.tzld.longarticle.recommend.server.common.response.CommonResponse;
+import com.tzld.longarticle.recommend.server.service.recommend.FilterContentByAccountService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+/**
+ * 内容账号归属过滤接口
+ *
+ * @author dyp
+ */
+@RestController
+@Slf4j
+public class FilterContentByAccountController {
+
+    @Autowired
+    private FilterContentByAccountService filterContentByAccountService;
+
+    @PostMapping("/filterContentByAccount")
+    public CommonResponse<List<String>> filterContentByAccount(@RequestBody FilterContentByAccountRequest request) {
+        List<String> channelContentIds = request.getChannelContentIds();
+        List<String> accountIds = request.getAccountIds();
+
+        log.info("filterContentByAccount, channelContentIds size:{}, accountIds size:{}",
+                channelContentIds != null ? channelContentIds.size() : 0,
+                accountIds != null ? accountIds.size() : 0);
+
+        List<String> filteredIds = filterContentByAccountService.filterContentByAccount(channelContentIds, accountIds);
+        return CommonResponse.success(filteredIds);
+    }
+}

+ 13 - 0
long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/web/recommend/FilterContentByAccountRequest.java

@@ -0,0 +1,13 @@
+package com.tzld.longarticle.recommend.server.web.recommend;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import java.util.List;
+
+@Getter
+@Setter
+public class FilterContentByAccountRequest {
+    private List<String> channelContentIds;
+    private List<String> accountIds;
+}

+ 21 - 2
long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/web/recommend/RecommendController.java

@@ -1,7 +1,11 @@
 package com.tzld.longarticle.recommend.server.web.recommend;
 
+import com.ctrip.framework.apollo.spring.annotation.ApolloJsonValue;
+import com.tzld.longarticle.recommend.server.common.enums.recommend.RankStrategyEnum;
 import com.tzld.longarticle.recommend.server.common.response.CommonResponse;
+import com.tzld.longarticle.recommend.server.model.param.PlanColdStartSortRequest;
 import com.tzld.longarticle.recommend.server.model.param.RecommendRequest;
+import com.tzld.longarticle.recommend.server.model.vo.PlanColdStartSortResponse;
 import com.tzld.longarticle.recommend.server.model.vo.RecommendResponse;
 import com.tzld.longarticle.recommend.server.model.vo.RecommendWithUserGroupResponse;
 import com.tzld.longarticle.recommend.server.service.recommend.RecommendService;
@@ -13,27 +17,42 @@ import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 
+import java.util.List;
+
 /**
  * @author dyp
  */
 @RestController
 @Slf4j
 public class RecommendController {
+
     @Autowired
     private RecommendService recommendService;
 
     @RequestMapping("/recommend")
     public RecommendResponse recommend(@RequestBody RecommendRequest httpRequest) {
-
+        if (StringUtils.hasText(httpRequest.getPlanTag())
+                && httpRequest.getPlanTag().contains(RankStrategyEnum.DEMAND_POOL_STRATEGY.getStrategy())) {
+            return recommendService.recommendDemandPool(httpRequest);
+        }
         return recommendService.recommend(httpRequest);
     }
 
     @RequestMapping("/recommend4FwhColdStart")
     public RecommendWithUserGroupResponse recommend4FwhColdStart(@RequestBody RecommendRequest httpRequest) {
-
         return recommendService.recommend4FwhColdStart(httpRequest);
     }
 
+    @RequestMapping("/recommendDemandPool")
+    public RecommendResponse recommendDemandPool(@RequestBody RecommendRequest httpRequest) {
+        return recommendService.recommendDemandPool(httpRequest);
+    }
+
+    @RequestMapping("/planColdStartSort")
+    public PlanColdStartSortResponse planColdStartSort(@RequestBody PlanColdStartSortRequest httpRequest) {
+        return recommendService.planColdStartSort(httpRequest);
+    }
+
     @RequestMapping("/deleteGzhWaiting")
     public CommonResponse<Void> deleteGzhWaiting(@RequestParam String planId,
                                                  @RequestParam String publishAccountId) {

+ 14 - 1
long-article-recommend-service/src/main/resources/application-dev.yml

@@ -134,4 +134,17 @@ pushMessage:
 small_page_url: https://testapi.piaoquantv.com
 
 pq:
-  host: https://videotest.yishihui.com
+  host: https://videotest.yishihui.com
+
+feign:
+  client:
+    config:
+      default:
+        logger-level: NONE
+        connect-timeout: 20000
+        read-timeout: 20000
+
+eureka:
+  client:
+    service-url:
+      defaultZone: http://deveureka-internal.piaoquantv.com/eureka/

+ 9 - 12
long-article-recommend-service/src/main/resources/application-pre.yml

@@ -1,17 +1,6 @@
 server:
   port: 8080
 
-eureka:
-  instance:
-    prefer-ip-address: true #是否优先使用IP地址作为主机名的标识,默认false
-    instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port} #注册到eureka上的唯一实例ID
-    lease-renewal-interval-in-seconds: 10 #表示eureka client发送心跳给server端的频率,默认30
-    lease-expiration-duration-in-seconds: 30 #表示eureka server至上一次收到client的心跳之后,等待下一次心跳的超时时间,在这个时间内若没收到下一次心跳,则将移除该instance,默认90
-  client:
-    registry-fetch-interval-seconds: 5  #定时从Eureka Server拉取服务注册信息的间隔时间
-    serviceUrl:
-      defaultZone: http://preeureka-internal.piaoquantv.com/eureka/
-
 spring:
   redis:
     hostName: r-bp1fogs2mflr1ybfot.redis.rds.aliyuncs.com
@@ -166,4 +155,12 @@ aliyun:
 small_page_url: https://testapi.piaoquantv.com
 
 pq:
-  host: https://videopre.piaoquantv.com
+  host: https://videopre.piaoquantv.com
+
+feign:
+  client:
+    config:
+      default:
+        logger-level: NONE
+        connect-timeout: 20000
+        read-timeout: 20000

+ 14 - 1
long-article-recommend-service/src/main/resources/application-prod.yml

@@ -124,4 +124,17 @@ pushMessage:
 small_page_url: https://api.piaoquantv.com
 
 pq:
-  host: https://longvideoapi.piaoquantv.com
+  host: https://longvideoapi.piaoquantv.com
+
+feign:
+  client:
+    config:
+      default:
+        logger-level: NONE
+        connect-timeout: 20000
+        read-timeout: 20000
+
+eureka:
+  client:
+    service-url:
+      defaultZone: http://eureka-internal.piaoquantv.com/eureka/

+ 14 - 1
long-article-recommend-service/src/main/resources/application-test.yml

@@ -151,4 +151,17 @@ aliyun:
 small_page_url: https://testapi.piaoquantv.com
 
 pq:
-  host: https://videotest.yishihui.com
+  host: https://videotest.yishihui.com
+
+feign:
+  client:
+    config:
+      default:
+        logger-level: NONE
+        connect-timeout: 20000
+        read-timeout: 20000
+
+eureka:
+  client:
+    service-url:
+      defaultZone: http://testeureka-internal.piaoquantv.com/eureka/

+ 9 - 0
long-article-recommend-service/src/main/resources/application.yml

@@ -57,6 +57,15 @@ apollo:
 mybatis:
   mapper-locations: classpath:/mapper/*.xml
 
+eureka:
+  instance:
+    prefer-ip-address: true
+    instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}
+    lease-renewal-interval-in-seconds: 30
+    lease-expiration-duration-in-seconds: 90
+  client:
+    registry-fetch-interval-seconds: 5
+
 rocketmq:
   accessKey: LTAI4G7puhXtLyHzHQpD6H7A
   secretKey: nEbq3xWNQd1qLpdy2u71qFweHkZjSG

File diff ditekan karena terlalu besar
+ 26 - 0
long-article-recommend-service/src/main/resources/file/长文_生成计划_账号需求利用池.json


+ 10 - 0
long-article-recommend-service/src/main/resources/mapper/aigc/AigcBaseMapper.xml

@@ -493,4 +493,14 @@
           and ppa.publish_open_flag = 1 and pps.push_type = 5
     </select>
 
+    <select id="getProducePlanExeRecordByChannelContentIds"
+            resultType="com.tzld.longarticle.recommend.server.model.entity.aigc.ProducePlanExeRecord">
+        select plan_exe_id, channel_content_id
+        from produce_plan_exe_record
+        where channel_content_id in
+        <foreach collection="list" item="item" open="(" close=")" separator=",">
+            #{item}
+        </foreach>
+    </select>
+
 </mapper>

+ 89 - 0
long-article-recommend-service/src/main/resources/mapper/longArticle/LongArticleBaseMapper.xml

@@ -555,4 +555,93 @@
         </if>
     </select>
 
+    <select id="getDemandSearchArticleRelations"
+            resultType="com.tzld.longarticle.recommend.server.model.dto.DemandSearchArticleRelationDTO">
+        select experiment_id   as experimentId,
+               channel_content_id as channelContentId
+        from demand_search_article_relation
+        where status = 2
+          and channel_content_id is not null
+    </select>
+
+    <insert id="batchInsertIgnoreDemandProduceContentRelation">
+        insert ignore into demand_produce_content_relation (content_id, channel_content_id, experiment_id, account_id)
+        values
+        <foreach collection="list" item="item" separator=",">
+            (#{item.contentId}, #{item.channelContentId}, #{item.experimentId}, #{item.accountId})
+        </foreach>
+    </insert>
+
+    <select id="getDemandExperimentIdByChannelContentIds"
+            resultType="com.tzld.longarticle.recommend.server.model.dto.DemandSearchArticleRelationDTO">
+        select channel_content_id as channelContentId,
+               experiment_id     as experimentId
+        from demand_search_article_relation
+        where status = 2
+          and channel_content_id in
+        <foreach collection="list" item="item" open="(" close=")" separator=",">
+            #{item}
+        </foreach>
+    </select>
+
+    <select id="getDemandPoolPromotionByLevel"
+            resultType="com.tzld.longarticle.recommend.server.model.dto.DemandSearchArticleRelationDTO">
+        select dps.channel_content_id      as channelContentId,
+               dps.root_produce_content_id as contentId,
+               coalesce(dpb.account_id, dps.account_id) as accountId
+        from demand_pool_promotion_source dps
+        left join demand_pool_promotion_bind dpb
+          on dps.channel_content_id = dpb.promotion_channel_content_id
+        where dps.level = #{level}
+          and dps.status = #{status}
+          and dps.deleted = #{deleted}
+    </select>
+
+    <select id="getDemandExperimentIdByContentIds"
+            resultType="com.tzld.longarticle.recommend.server.model.dto.DemandSearchArticleRelationDTO">
+        select content_id       as contentId,
+               experiment_id    as experimentId
+        from demand_produce_content_relation
+        where content_id in
+        <foreach collection="contentIds" item="item" open="(" close=")" separator=",">
+            #{item}
+        </foreach>
+        <if test="accountId != null and accountId != ''">
+          and (account_id = #{accountId} or account_id is null)
+        </if>
+    </select>
+
+    <select id="getAccountByExperimentIds"
+            resultType="com.tzld.longarticle.recommend.server.model.dto.DemandSearchArticleRelationDTO">
+        select experiment_id as experimentId,
+               account       as accountId
+        from demand_search_queue
+        where experiment_id in
+        <foreach collection="list" item="item" open="(" close=")" separator=",">
+            #{item}
+        </foreach>
+    </select>
+
+    <select id="getByPlanIdAndAccountIdsAndContentPoolType"
+            resultType="com.tzld.longarticle.recommend.server.model.entity.longArticle.PublishContentGzhWaiting">
+        select * from publish_content_gzh_waiting
+        where plan_id = #{planId}
+          and publish_account_id in
+        <foreach collection="accountIds" item="item" open="(" close=")" separator=",">
+            #{item}
+        </foreach>
+          and content_pool_type = #{contentPoolType}
+          and status = 1
+    </select>
+
+    <update id="batchUpdateContentStatusBySourceIds">
+        update publish_content_gzh_waiting
+        set status = #{status}, update_timestamp = #{updateTimestamp}
+        where source_id in
+        <foreach collection="sourceIds" item="item" open="(" close=")" separator=",">
+            #{item}
+        </foreach>
+    </update>
+
+
 </mapper>

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini