Przeglądaj źródła

需求内容池 新排序、晋级

wangyunpeng 16 godzin temu
rodzic
commit
96f0ab510e
23 zmienionych plików z 1029 dodań i 25 usunięć
  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. 14 0
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/mapper/longArticle/LongArticleBaseMapper.java
  4. 39 0
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/model/entity/longArticle/AccountDemandUtilizePlan.java
  5. 45 0
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/model/entity/longArticle/DemandPoolPromotionBind.java
  6. 30 0
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/model/param/PlanColdStartSortRequest.java
  7. 1 0
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/model/param/RecommendRequest.java
  8. 66 0
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/model/vo/PlanColdStartSortResponse.java
  9. 135 0
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/remote/aigc/AIGCProducePlanCreateService.java
  10. 14 0
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/repository/longArticle/AccountDemandUtilizePlanRepository.java
  11. 14 0
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/repository/longArticle/DemandPoolPromotionBindRepository.java
  12. 6 0
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/service/XxlJobService.java
  13. 179 0
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/service/recommend/ArticlePromotionService.java
  14. 305 0
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/service/recommend/RecommendService.java
  15. 36 8
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/service/recommend/rank/strategy/DemandPoolRankStrategy.java
  16. 10 2
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/web/recommend/RecommendController.java
  17. 14 1
      long-article-recommend-service/src/main/resources/application-dev.yml
  18. 9 12
      long-article-recommend-service/src/main/resources/application-pre.yml
  19. 14 1
      long-article-recommend-service/src/main/resources/application-prod.yml
  20. 14 1
      long-article-recommend-service/src/main/resources/application-test.yml
  21. 9 0
      long-article-recommend-service/src/main/resources/application.yml
  22. 26 0
      long-article-recommend-service/src/main/resources/file/长文_生成计划_账号需求利用池.json
  23. 21 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);

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

@@ -144,4 +144,18 @@ public interface LongArticleBaseMapper {
     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);
+
 }

+ 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;
+}

+ 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;
+    }
+}

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

@@ -0,0 +1,135 @@
+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 = String.format("账号需求池_%s_利用池", accountName);
+
+        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;
+    }
+}

+ 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);
+}

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

@@ -0,0 +1,14 @@
+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;
+
+/**
+ * @author dyp
+ */
+@Repository
+public interface DemandPoolPromotionBindRepository extends JpaRepository<DemandPoolPromotionBind, Long> {
+
+    DemandPoolPromotionBind getByPromotionChannelContentIdAndAccountId(String promotionChannelContentId, String accountId);
+}

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

@@ -853,6 +853,12 @@ public class XxlJobService {
         return ReturnT.SUCCESS;
     }
 
+    @XxlJob("demandPoolPromotionBind")
+    public ReturnT<String> demandPoolPromotionBind(String param) {
+        articlePromotionService.demandPoolPromotionBind();
+        return ReturnT.SUCCESS;
+    }
+
     @XxlJob("refreshArticleHisCache")
     public ReturnT<String> refreshArticleHisCache(String param) {
         // 刷新历史表现缓存

+ 179 - 0
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;
@@ -92,6 +93,12 @@ public class ArticlePromotionService {
     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;
@@ -784,4 +791,176 @@ public class ArticlePromotionService {
         return detail;
     }
 
+    /**
+     * 需求池晋级绑定:将已完成溯源的晋级记录绑定到对应账号的 accountDemandPoolLevelUtilize 生成计划。
+     * <p>
+     * 流程:
+     * 1. 读取 demand_pool_promotion_source(status=FINISH, 未绑定)
+     * 2. 对每条记录获取 publishContentId → 创建抓取计划
+     * 3. 查找/创建账号的 accountDemandPoolLevelUtilize 生成计划
+     * 4. 将抓取计划绑定到生成计划
+     */
+    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;
+
+        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. 查找已有生成计划映射;存在则绑定抓取计划,不存在则创建(抓取计划注入到 inputSources 中)
+                String producePlanId = null;
+                AccountDemandUtilizePlan mapping = accountDemandUtilizePlanRepository.getByAccountId(accountId);
+                boolean hasExistingPlan = mapping != null && StringUtils.hasText(mapping.getProducePlanId());
+
+                if (hasExistingPlan) {
+                    // 校验计划是否仍存在
+                    ProducePlanDetailVO existDetail = getProducePlanDetailRetry(mapping.getProducePlanId());
+                    if (existDetail != null) {
+                        // 计划存在 → 绑定抓取计划
+                        producePlanId = mapping.getProducePlanId();
+                        String inputSourceLabel = String.format("需求池晋级-长文-微信公众号-内容添加计划-%s", crawlerPlanInfo.getName());
+                        articleAddDependPlan(producePlanId, crawlerPlanInfo.getId(), inputSourceLabel,
+                                ProducePlanInputSourceTypeEnum.contentPlan.getVal());
+                        log.info("demandPoolPromotionBind bind to existing plan, accountId:{} planId:{} crawlerPlanId:{}",
+                                accountId, producePlanId, crawlerPlanInfo.getId());
+                    } else {
+                        // 计划已被删除 → 重新创建
+                        log.warn("demandPoolPromotionBind existing plan not found, recreate. accountId:{} oldPlanId:{}",
+                                accountId, mapping.getProducePlanId());
+                        hasExistingPlan = false;
+                    }
+                }
+
+                if (!hasExistingPlan) {
+                    // 创建生成计划(抓取计划 ID 直接注入输入源,无需再调 articleAddDependPlan)
+                    IdNameVO<String> planInfo = aigcProducePlanCreateService.createUtilizeProducePlan(
+                            accountName, crawlerPlanInfo.getId(), crawlerPlanInfo.getName());
+                    if (planInfo == null || !StringUtils.hasText(planInfo.getId())) {
+                        log.error("demandPoolPromotionBind create produce plan failed, accountId:{} accountName:{}",
+                                accountId, accountName);
+                        skipCount++;
+                        continue;
+                    }
+                    producePlanId = planInfo.getId();
+
+                    // 保存映射
+                    if (mapping == null) {
+                        mapping = new AccountDemandUtilizePlan();
+                    }
+                    mapping.setAccountId(accountId);
+                    mapping.setProducePlanId(planInfo.getId());
+                    mapping.setPlanName(planInfo.getName());
+                    mapping.setCreatedAt(now);
+                    mapping.setUpdatedAt(now);
+                    accountDemandUtilizePlanRepository.save(mapping);
+
+                }
+
+                if (producePlanId == 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. 记录绑定关系
+                DemandPoolPromotionBind bind = new DemandPoolPromotionBind();
+                bind.setPromotionChannelContentId(record.getChannelContentId());
+                bind.setAccountId(accountId);
+                bind.setRootProduceContentId(record.getRootProduceContentId());
+                bind.setCrawlerPlanId(crawlerPlanInfo.getId());
+                bind.setProducePlanId(producePlanId);
+                bind.setCreatedAt(now);
+                bind.setUpdatedAt(now);
+                demandPoolPromotionBindRepository.save(bind);
+
+                bindCount++;
+                log.info("demandPoolPromotionBind success, channelContentId:{} accountId:{} crawlerPlanId:{} producePlanId:{}",
+                        record.getChannelContentId(), accountId, crawlerPlanInfo.getId(), producePlanId);
+
+            } 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;
+    }
+
 }

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

@@ -21,10 +21,12 @@ import com.tzld.longarticle.recommend.server.model.dto.DemandSearchArticleRelati
 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;
@@ -104,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();
@@ -834,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 缓存。

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

@@ -3,6 +3,7 @@ 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;
@@ -32,6 +33,9 @@ public class DemandPoolRankStrategy implements RankStrategy {
     @Autowired
     private ScoreService scoreService;
 
+    @Autowired
+    private StrategyIndexScoreWeightService weightService;
+
     @Override
     public RankResult rank(RankParam param) {
 
@@ -49,17 +53,41 @@ public class DemandPoolRankStrategy implements RankStrategy {
             RankItem item = new RankItem();
             c.setHisPublishArticleList(null);
             item.setContent(c);
+            c.setScoreMap(scoreMap.get(c.getId()));
             item.setScoreMap(scoreMap.get(c.getId()));
 
-            double score = item.getScore(ScoreStrategyEnum.VIEW_COUNT.value())
-                    + item.getScore(ScoreStrategyEnum.VIEW_COUNT_RATE.value())
-                    + item.getScore(ScoreStrategyEnum.CATEGORY.value())
-                    + item.getScore(ScoreStrategyEnum.FLOW_CTL_DECREASE.value());
-            item.setScore(score);
-
-            if (utilizePool.equals(c.getContentPoolType())) {
+            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(c.getContentPoolType())) {
+            } 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);
             }
         }

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

@@ -1,8 +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;
@@ -31,8 +34,8 @@ public class RecommendController {
 
     @RequestMapping("/recommend")
     public RecommendResponse recommend(@RequestBody RecommendRequest httpRequest) {
-        if (StringUtils.hasText(httpRequest.getAccountId())
-                && recommendDemandPoolAccounts.contains(httpRequest.getAccountId())) {
+        if (StringUtils.hasText(httpRequest.getPlanTag())
+                && httpRequest.getPlanTag().contains(RankStrategyEnum.DEMAND_POOL_STRATEGY.getStrategy())) {
             return recommendService.recommendDemandPool(httpRequest);
         }
         return recommendService.recommend(httpRequest);
@@ -48,6 +51,11 @@ public class RecommendController {
         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

Plik diff jest za duży
+ 26 - 0
long-article-recommend-service/src/main/resources/file/长文_生成计划_账号需求利用池.json


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

@@ -609,5 +609,26 @@
         </if>
     </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>

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików