Sfoglia il codice sorgente

长文供需看板-全局 导出

wangyunpeng 11 ore fa
parent
commit
a7d01ab011
12 ha cambiato i file con 485 aggiunte e 19 eliminazioni
  1. 2 0
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/mapper/aigc/AigcBaseMapper.java
  2. 1 0
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/model/dto/ProduceContentDTO.java
  3. 62 0
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/model/vo/LongArticlesSupplyAndDemandExport.java
  4. 2 0
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/repository/aigc/ProducePlanRepository.java
  5. 2 0
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/repository/longArticle/ArticleCategoryRepository.java
  6. 2 0
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/repository/longArticle/DatastatSortStrategyRepository.java
  7. 2 0
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/repository/longArticle/LongArticleTitleAuditRepository.java
  8. 331 17
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/service/recommend/DataDashboardService.java
  9. 29 0
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/util/feishu/FeiShu.java
  10. 31 0
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/util/feishu/FeishuMessageSender.java
  11. 7 0
      long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/web/recommend/DataDashboardController.java
  12. 14 2
      long-article-recommend-service/src/main/resources/mapper/aigc/AigcBaseMapper.xml

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

@@ -31,6 +31,8 @@ public interface AigcBaseMapper {
 
 
     List<ProducePlanExeRecord> getAllByProducePlanId(List<String> producePlanIds);
     List<ProducePlanExeRecord> getAllByProducePlanId(List<String> producePlanIds);
 
 
+    List<ProducePlanExeRecord> getAllPassContentByProducePlanId(List<String> producePlanIds);
+
     List<CrawlerPlan> getColdCrawlerPlan(Long timeStart, Long timeEnd, List<String> planTags);
     List<CrawlerPlan> getColdCrawlerPlan(Long timeStart, Long timeEnd, List<String> planTags);
 
 
     List<ProducePlanAuditCheckDTO> getProducePlanAudit(Long timeStart, Long timeEnd, List<String> planIds);
     List<ProducePlanAuditCheckDTO> getProducePlanAudit(Long timeStart, Long timeEnd, List<String> planIds);

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

@@ -8,6 +8,7 @@ import lombok.Setter;
 public class ProduceContentDTO {
 public class ProduceContentDTO {
 
 
     private String contentId;
     private String contentId;
+    private String planExeId;
     private String title;
     private String title;
     private String bodyText;
     private String bodyText;
     private Long produceTimestamp;
     private Long produceTimestamp;

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

@@ -0,0 +1,62 @@
+package com.tzld.longarticle.recommend.server.model.vo;
+
+import lombok.Data;
+
+@Data
+public class LongArticlesSupplyAndDemandExport {
+
+    // 日期
+    private String dateStr;
+    /**
+     * 1. 知识科普
+     * 2. 军事历史
+     * 3. 家长里短
+     * 4. 社会法治
+     * 5. 奇闻趣事
+     * 6. 名人八卦
+     * 7. 健康养生
+     * 8. 情感故事
+     * 9. 国家大事
+     * 10. 现代人物
+     * 11. 怀旧时光
+     * 12. 政治新闻
+     * 13. 历史人物
+     * 14. 社会现象
+     * 15. 财经科技
+     */
+    // 长文品类
+    private String category;
+    // 头条内容池量
+    private Long firstLevelContentPoolCount;
+    // 头条内容池量(去重)
+    private Long firstLevelContentPoolDedupCount;
+    // 头条内容池占比
+    private Double firstLevelContentPoolRate;
+    // 阅读量
+    private Long readCount;
+    // 消费占比
+    private Double consumptionRate;
+    // 供需比 =头条内容池占比/(消费占比+0.1%)
+    private Double supplyDemandRate;
+    // push粉丝量
+    private Long pushFansCount;
+    // push阅读量
+    private Long pushReadCount;
+    // push阅读率
+    private Double pushReadRate;
+    // 发布条数
+    private Long publishCount;
+    // 发布条数占比
+    private Double publishCountRate;
+    // push粉丝量占比
+    private Double pushFansCountRate;
+    // 需曝比 =消费占比/push粉丝量占比
+    private Double demandExposureRate;
+    // 供需分析 =IF($H2>=1.1,"供给足",IF($H2<=0.9,"供给缺","供需平衡"))
+    private String supplyDemandAnalysis;
+    // push质量分析 =IF(K3>=0.75%,"高质量",IF(AND(K3>=0.5%,K3<0.75%),"中质量","低质量"))
+    private String pushQualityAnalysis;
+    // 供需举措 =IF(Q2="低质量","提质",IF(Q2="高质量","保质",""))&IF(P2="供给足","保持供给",IF(P2="供给缺","提供给",""))
+    private String supplyDemandAction;
+
+}

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

@@ -11,4 +11,6 @@ public interface ProducePlanRepository extends JpaRepository<ProducePlan, String
 
 
     List<ProducePlan> findByIdIn(List<String> planIds);
     List<ProducePlan> findByIdIn(List<String> planIds);
 
 
+    List<ProducePlan> findByPlanTagLike(String planTag);
+
 }
 }

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

@@ -13,6 +13,8 @@ public interface ArticleCategoryRepository extends JpaRepository<ArticleCategory
 
 
     List<ArticleCategory> getByProduceContentIdInAndVersion(List<String> produceContentIds, Integer version);
     List<ArticleCategory> getByProduceContentIdInAndVersion(List<String> produceContentIds, Integer version);
 
 
+    List<ArticleCategory> getByProduceContentIdInAndVersionAndStatus(List<String> produceContentIds, Integer version, Integer status);
+
     ArticleCategory getByProduceContentIdAndVersion(String produceContentId, Integer version);
     ArticleCategory getByProduceContentIdAndVersion(String produceContentId, Integer version);
 
 
     List<ArticleCategory> getByStatusAndVersion(Integer status, Integer version);
     List<ArticleCategory> getByStatusAndVersion(Integer status, Integer version);

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

@@ -16,4 +16,6 @@ public interface DatastatSortStrategyRepository extends JpaRepository<DatastatSo
     List<DatastatSortStrategy> getByWxSnIn(List<String> wxSnList);
     List<DatastatSortStrategy> getByWxSnIn(List<String> wxSnList);
 
 
     List<DatastatSortStrategy> getByDateStr(String dateStr);
     List<DatastatSortStrategy> getByDateStr(String dateStr);
+
+    List<DatastatSortStrategy> getByDateStrAndPositionEquals(String dateStr, Integer position);
 }
 }

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

@@ -27,6 +27,8 @@ public interface LongArticleTitleAuditRepository extends JpaRepository<LongArtic
 
 
     List<LongArticleTitleAudit> getByContentIdIn(List<String> sourceIds);
     List<LongArticleTitleAudit> getByContentIdIn(List<String> sourceIds);
 
 
+    List<LongArticleTitleAudit> getByContentIdInAndStatus(List<String> contentIds, Integer status);
+
     List<LongArticleTitleAudit> getByAuditTimestampBetween(Long start, Long end);
     List<LongArticleTitleAudit> getByAuditTimestampBetween(Long start, Long end);
 
 
     List<LongArticleTitleAudit> getByFlowPoolLevelIsNull();
     List<LongArticleTitleAudit> getByFlowPoolLevelIsNull();

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

@@ -231,7 +231,7 @@ public class DataDashboardService {
                         Pair.of("S", "0.00%"),
                         Pair.of("S", "0.00%"),
                         Pair.of("T", "0.00%")
                         Pair.of("T", "0.00%")
                 );
                 );
-        doSendFeishuSheet(dateStrList, sheetToken, sheetId, rowNum, rows, 2, styles, delDateStrList, null);
+        doSendFeishuSheet(FeiShu.requestAccessToken(), dateStrList, sheetToken, sheetId, rowNum, rows, 2, styles, delDateStrList, null);
     }
     }
 
 
     public List<NewSortStrategyExport> newSortStrategyData(String beginDate, String endDate,
     public List<NewSortStrategyExport> newSortStrategyData(String beginDate, String endDate,
@@ -855,12 +855,11 @@ public class DataDashboardService {
     }
     }
 
 
 
 
-    private static void doSendFeishuSheet(List<String> dateStrList, String sheetToken, String sheetId,
+    private static void doSendFeishuSheet(Pair<String, Integer> token, List<String> dateStrList, String sheetToken, String sheetId,
                                           int rowNum, List<List<Object>> rows, Integer startRowIndex,
                                           int rowNum, List<List<Object>> rows, Integer startRowIndex,
                                           List<Pair<String, String>> styles,
                                           List<Pair<String, String>> styles,
                                           List<String> delDateStrList,
                                           List<String> delDateStrList,
                                           List<Pair<String, List<Pair<String, String>>>> thanks) {
                                           List<Pair<String, List<Pair<String, String>>>> thanks) {
-        Pair<String, Integer> token = FeiShu.requestAccessToken();
         RestTemplate restTemplate = new RestTemplate();
         RestTemplate restTemplate = new RestTemplate();
         HttpHeaders httpHeaders = new HttpHeaders();
         HttpHeaders httpHeaders = new HttpHeaders();
         httpHeaders.add("Authorization", "Bearer " + token.getFirst());
         httpHeaders.add("Authorization", "Bearer " + token.getFirst());
@@ -1132,7 +1131,7 @@ public class DataDashboardService {
                         Pair.of("S", "0.00%"),
                         Pair.of("S", "0.00%"),
                         Pair.of("T", "0.00%")
                         Pair.of("T", "0.00%")
                 );
                 );
-        doSendFeishuSheet(dateStrList, sheetToken, sheetId, rowNum, rows, 2, styles, delDateStrList, null);
+        doSendFeishuSheet(FeiShu.requestAccessToken(), dateStrList, sheetToken, sheetId, rowNum, rows, 2, styles, delDateStrList, null);
     }
     }
 
 
     public List<NewSortStrategyExport> newSortStrategyFWHData(String beginDate, String endDate,
     public List<NewSortStrategyExport> newSortStrategyFWHData(String beginDate, String endDate,
@@ -1498,7 +1497,7 @@ public class DataDashboardService {
                         Pair.of("BP", "0.00%")
                         Pair.of("BP", "0.00%")
                 );
                 );
 
 
-        doSendFeishuSheet(dateStrList, sheetToken, sheetId, rowNum, rows, 3, styles, null, null);
+        doSendFeishuSheet(FeiShu.requestAccessToken(), dateStrList, sheetToken, sheetId, rowNum, rows, 3, styles, null, null);
     }
     }
 
 
     private void checkViewCountFeishuAlarm(List<IntermediateIndicatorsExport> newContentsYesData) {
     private void checkViewCountFeishuAlarm(List<IntermediateIndicatorsExport> newContentsYesData) {
@@ -2129,7 +2128,7 @@ public class DataDashboardService {
                         Pair.of("AD", "#,##0.00")
                         Pair.of("AD", "#,##0.00")
                 );
                 );
 
 
-        doSendFeishuSheet(dateStrList, sheetToken, sheetId, rowNum, rows, 2, styles, null, null);
+        doSendFeishuSheet(FeiShu.requestAccessToken(), dateStrList, sheetToken, sheetId, rowNum, rows, 2, styles, null, null);
     }
     }
 
 
     private List<FirstContentScoreExport> firstContentScoreData(List<String> dateStrList) {
     private List<FirstContentScoreExport> firstContentScoreData(List<String> dateStrList) {
@@ -2337,7 +2336,7 @@ public class DataDashboardService {
                         Pair.of("I", "0%")
                         Pair.of("I", "0%")
                 );
                 );
 
 
-        doSendFeishuSheet(dateStrList, dailyDetailSheetToken, "28RgAZ", rowNum, rows,
+        doSendFeishuSheet(FeiShu.requestAccessToken(), dateStrList, dailyDetailSheetToken, "28RgAZ", rowNum, rows,
                 2, styles, null, null);
                 2, styles, null, null);
     }
     }
 
 
@@ -2495,7 +2494,7 @@ public class DataDashboardService {
             }
             }
         }
         }
 
 
-        doSendFeishuSheet(dateStrList, dailySafeSheetToken, "9fc2e8", rowNum, rows,
+        doSendFeishuSheet(FeiShu.requestAccessToken(), dateStrList, dailySafeSheetToken, "9fc2e8", rowNum, rows,
                 2, null, null, null);
                 2, null, null, null);
     }
     }
 
 
@@ -2570,7 +2569,7 @@ public class DataDashboardService {
                         Pair.of("K", "0.00%")
                         Pair.of("K", "0.00%")
                 );
                 );
 
 
-        doSendFeishuSheet(dateStrList, dailyDetailSheetToken, "vddANt", rowNum, rows,
+        doSendFeishuSheet(FeiShu.requestAccessToken(), dateStrList, dailyDetailSheetToken, "vddANt", rowNum, rows,
                 2, styles, null, null);
                 2, styles, null, null);
     }
     }
 
 
@@ -2712,7 +2711,7 @@ public class DataDashboardService {
                         Pair.of("U", "0.00%"),
                         Pair.of("U", "0.00%"),
                         Pair.of("V", "0.00%")
                         Pair.of("V", "0.00%")
                 );
                 );
-        doSendFeishuSheet(dateStrList, dailyDetailSheetToken, "qEipyL", rowNum, rows,
+        doSendFeishuSheet(FeiShu.requestAccessToken(), dateStrList, dailyDetailSheetToken, "qEipyL", rowNum, rows,
                 4, styles, null, null);
                 4, styles, null, null);
     }
     }
 
 
@@ -2872,7 +2871,7 @@ public class DataDashboardService {
                                 Pair.of("搜狐视频", "#D9F5D6"),
                                 Pair.of("搜狐视频", "#D9F5D6"),
                                 Pair.of("票圈视频", "#F8DEF8")))
                                 Pair.of("票圈视频", "#F8DEF8")))
                 );
                 );
-        doSendFeishuSheet(dateStrList, dailyDetailSheetToken, "6aW60b", rowNum, rows,
+        doSendFeishuSheet(FeiShu.requestAccessToken(), dateStrList, dailyDetailSheetToken, "6aW60b", rowNum, rows,
                 3, styles, null, thank);
                 3, styles, null, thank);
     }
     }
 
 
@@ -3147,7 +3146,7 @@ public class DataDashboardService {
                                 Pair.of("次条", "#B1E8FC"),
                                 Pair.of("次条", "#B1E8FC"),
                                 Pair.of("3-8", "#F8E6AB")))
                                 Pair.of("3-8", "#F8E6AB")))
                 );
                 );
-        doSendFeishuSheet(dateStrList, dailyDetailSheetToken, "qvxJsD", rowNum, rows,
+        doSendFeishuSheet(FeiShu.requestAccessToken(), dateStrList, dailyDetailSheetToken, "qvxJsD", rowNum, rows,
                 2, styles, null, thank);
                 2, styles, null, thank);
     }
     }
 
 
@@ -3514,7 +3513,7 @@ public class DataDashboardService {
                 }
                 }
             }
             }
         }
         }
-        doSendFeishuSheet(dateStrList, dailyDetailSheetToken, "jwayC4", rowNum, rows,
+        doSendFeishuSheet(FeiShu.requestAccessToken(), dateStrList, dailyDetailSheetToken, "jwayC4", rowNum, rows,
                 2, null, null, null);
                 2, null, null, null);
     }
     }
 
 
@@ -3542,7 +3541,7 @@ public class DataDashboardService {
                 }
                 }
             }
             }
         }
         }
-        doSendFeishuSheet(dateStrList, dailyDetailSheetToken, "QiEZiz", rowNum, rows,
+        doSendFeishuSheet(FeiShu.requestAccessToken(), dateStrList, dailyDetailSheetToken, "QiEZiz", rowNum, rows,
                 2, null, null, null);
                 2, null, null, null);
     }
     }
 
 
@@ -4095,7 +4094,7 @@ public class DataDashboardService {
                                 Pair.of("长文审核", "#BACEFD"),
                                 Pair.of("长文审核", "#BACEFD"),
                                 Pair.of("文章内容池", "#FED4A4")))
                                 Pair.of("文章内容池", "#FED4A4")))
                 );
                 );
-        doSendFeishuSheet(dateStrList, dailyDetailSheetToken, "Lushkl", rowNum, rows,
+        doSendFeishuSheet(FeiShu.requestAccessToken(), dateStrList, dailyDetailSheetToken, "Lushkl", rowNum, rows,
                 3, styles, null, thank);
                 3, styles, null, thank);
     }
     }
 
 
@@ -4174,7 +4173,7 @@ public class DataDashboardService {
                         Pair.of("L", "0.00%"),
                         Pair.of("L", "0.00%"),
                         Pair.of("M", "0.00%")
                         Pair.of("M", "0.00%")
                 );
                 );
-        doSendFeishuSheet(dateStrList, dailyDetailSheetToken, "bl1eL2", rowNum, rows,
+        doSendFeishuSheet(FeiShu.requestAccessToken(), dateStrList, dailyDetailSheetToken, "bl1eL2", rowNum, rows,
                 2, styles, null, null);
                 2, styles, null, null);
     }
     }
 
 
@@ -4347,7 +4346,7 @@ public class DataDashboardService {
                         Pair.of("Q", "0.00%"),
                         Pair.of("Q", "0.00%"),
                         Pair.of("R", "0%")
                         Pair.of("R", "0%")
                 );
                 );
-        doSendFeishuSheet(dateStrList, dailyDetailSheetToken, "0Lwz8A", rowNum, rows,
+        doSendFeishuSheet(FeiShu.requestAccessToken(), dateStrList, dailyDetailSheetToken, "0Lwz8A", rowNum, rows,
                 2, styles, null, null);
                 2, styles, null, null);
     }
     }
 
 
@@ -4455,4 +4454,319 @@ public class DataDashboardService {
         bd = bd.setScale(places, RoundingMode.HALF_UP);
         bd = bd.setScale(places, RoundingMode.HALF_UP);
         return bd.doubleValue();
         return bd.doubleValue();
     }
     }
+
+    @XxlJob("contentSupplyAndDemandDashBoardJob")
+    public ReturnT<String> contentSupplyAndDemandDashBoardJob(String param) {
+        List<String> dateStrList = DateUtils.getBeforeDays(null, null, 1);
+        contentSupplyAndDemandDashBoard(dateStrList);
+        return ReturnT.SUCCESS;
+    }
+
+    public void contentSupplyAndDemandDashBoard(String dateStr) {
+        if (!StringUtils.hasText(dateStr)) {
+            dateStr = DateUtils.getBeforeDaysDateStr("yyyyMMdd", 1);
+        }
+        contentSupplyAndDemandDashBoard(Collections.singletonList(dateStr));
+    }
+
+    public void contentSupplyAndDemandDashBoard(List<String> dateStrList) {
+        List<LongArticlesSupplyAndDemandExport> exportList = new ArrayList<>();
+        dateStrList = Lists.reverse(dateStrList);
+        for (String dateStr : dateStrList) {
+            exportList.addAll(buildContentSupplyAndDemandExport(dateStr));
+        }
+        if (CollectionUtil.isEmpty(exportList)) {
+            return;
+        }
+        int rowNum = exportList.size();
+        List<List<Object>> rows = new ArrayList<>();
+        Field[] fields = LongArticlesSupplyAndDemandExport.class.getDeclaredFields();
+        for (LongArticlesSupplyAndDemandExport datum : exportList) {
+            List<Object> rowDatas = new ArrayList<>();
+            rows.add(rowDatas);
+
+            for (Field field : fields) {
+                field.setAccessible(true);
+                try {
+                    rowDatas.add(field.get(datum));
+                } catch (IllegalAccessException e) {
+                    log.error("获取值出错:{}", field.getName());
+                } catch (Exception e) {
+                    throw new RuntimeException(e.getMessage());
+                }
+            }
+        }
+
+        List<Pair<String, String>> styles = Arrays
+                .asList(
+                        Pair.of("E", "0.00%"),
+                        Pair.of("G", "0.00%"),
+                        Pair.of("K", "0.00%"),
+                        Pair.of("M", "0.00%"),
+                        Pair.of("N", "0.00%"),
+                        Pair.of("O", "0.00%")
+                );
+        doSendFeishuSheet(FeiShu.requestPQAccessToken(), dateStrList, "FD47w23TaiITHSkKCmqcM85FnBd", "fd59c2", rowNum, rows,
+                2, styles, null, null);
+    }
+
+    private List<LongArticlesSupplyAndDemandExport> buildContentSupplyAndDemandExport(String dateStr) {
+        List<LongArticlesSupplyAndDemandExport> result = new ArrayList<>();
+        Long start = DateUtils.getStartOfDay(dateStr, "yyyyMMdd") * 1000;
+        Long end = start + 86400000;
+
+        // 定义固定的15个品类
+        List<String> categoryList = Arrays.asList(
+                "知识科普", "军事历史", "家长里短", "社会法治", "奇闻趣事",
+                "名人八卦", "健康养生", "情感故事", "国家大事", "现代人物",
+                "怀旧时光", "政治新闻", "历史人物", "社会现象", "财经科技"
+        );
+
+        // 1. 查询头条生成计划 plan_tag='autoArticlePoolLevel1'
+        List<ProducePlan> firstLevelPlans = producePlanRepository.findByPlanTagLike("autoArticlePoolLevel1");
+        List<String> firstLevelPlanIds = firstLevelPlans.stream().map(ProducePlan::getId).collect(Collectors.toList());
+
+        // 2. 根据生成计划查询所有内容 produce_plan_exe_record
+        List<ProducePlanExeRecord> exeRecordList = aigcBaseMapper.getAllPassContentByProducePlanId(firstLevelPlanIds);
+        List<String> planExeIds = exeRecordList.stream()
+                .map(ProducePlanExeRecord::getPlanExeId).collect(Collectors.toList());
+
+        // 3. 视频审核过滤 - 分批查询审核通过的内容
+        Set<String> auditPassContentIds = new HashSet<>();
+        if (CollectionUtils.isNotEmpty(planExeIds)) {
+            for (List<String> partition : Lists.partition(planExeIds, 1000)) {
+                List<LongArticleTitleAudit> auditList = longArticleTitleAuditRepository
+                        .getByContentIdInAndStatus(partition, ArticleVideoAuditStatusEnum.PASS.getCode());
+                auditPassContentIds.addAll(auditList.stream()
+                        .map(LongArticleTitleAudit::getContentId)
+                        .collect(Collectors.toSet()));
+            }
+        }
+
+        // 4. 根据审核通过的内容ID查询内容标题(用于后续去重)
+        Map<String, String> contentIdTitleMap = new HashMap<>();
+        if (CollectionUtils.isNotEmpty(auditPassContentIds)) {
+            List<String> auditPassContentIdList = new ArrayList<>(auditPassContentIds);
+            for (List<String> partition : Lists.partition(auditPassContentIdList, 1000)) {
+                List<ProduceContentDTO> contentList = aigcBaseMapper.getProduceContentByPlanExeIds(partition);
+                contentIdTitleMap.putAll(contentList.stream()
+                        .collect(Collectors.toMap(ProduceContentDTO::getPlanExeId, ProduceContentDTO::getTitle, (a, b) -> a)));
+            }
+        }
+
+        // 5. 查询所有内容品类 - 使用内容ID分批查询
+        List<ArticleCategory> articleCategoryList = new ArrayList<>();
+        if (CollectionUtils.isNotEmpty(auditPassContentIds)) {
+            List<String> auditPassContentIdList = new ArrayList<>(auditPassContentIds);
+            for (List<String> partition : Lists.partition(auditPassContentIdList, 1000)) {
+                articleCategoryList.addAll(articleCategoryRepository.getByProduceContentIdInAndVersionAndStatus(
+                        partition, activeVersion, ArticleCategoryStatusEnum.SUCCESS.getCode()));
+            }
+        }
+        Map<String, ArticleCategory> categoryMap = articleCategoryList.stream()
+                .collect(Collectors.toMap(ArticleCategory::getProduceContentId, Function.identity(), (a, b) -> a));
+
+        // 6. 计算头条内容池量(历史所有审核通过的内容,按品类分组)
+        Map<String, Long> categoryPoolCountMap = new HashMap<>();
+        Map<String, Set<String>> categoryPoolTitleSetMap = new HashMap<>();
+        for (String category : categoryList) {
+            categoryPoolCountMap.put(category, 0L);
+            categoryPoolTitleSetMap.put(category, new HashSet<>());
+        }
+        long totalFirstLevelContentPoolCount = 0L;
+        Set<String> totalPoolTitleSet = new HashSet<>();
+
+        for (String contentId : auditPassContentIds) {
+            ArticleCategory articleCategory = categoryMap.get(contentId);
+            if (Objects.isNull(articleCategory)) {
+                continue;
+            }
+            String category = articleCategory.getCategory();
+            if (!categoryPoolCountMap.containsKey(category)) {
+                continue;
+            }
+            // 不去重计数
+            categoryPoolCountMap.put(category, categoryPoolCountMap.get(category) + 1);
+            totalFirstLevelContentPoolCount++;
+            // 去重计数(按标题)
+            String contentTitle = contentIdTitleMap.get(contentId);
+            if (StringUtils.hasText(contentTitle)) {
+                categoryPoolTitleSetMap.get(category).add(contentTitle);
+                totalPoolTitleSet.add(contentTitle);
+            }
+        }
+
+        // 7. 查询当日发送条数、品类 datastat_sort_strategy
+        List<DatastatSortStrategy> sortStrategyList = datastatSortStrategyRepository.getByDateStrAndPositionEquals(dateStr, 1);
+        List<DatastatSortStrategy> firstLevelSortList = sortStrategyList.stream()
+                .filter(o -> Objects.equals(o.getPosition(), 1))
+                .collect(Collectors.toList());
+
+        // 8. 按品类分组统计当日发送数据
+        Map<String, LongArticlesSupplyAndDemandExport> categoryExportMap = new HashMap<>();
+
+        // 初始化所有品类
+        for (String category : categoryList) {
+            LongArticlesSupplyAndDemandExport export = new LongArticlesSupplyAndDemandExport();
+            export.setDateStr(dateStr);
+            export.setCategory(category);
+            // 头条内容池量 - 来自历史所有审核通过的内容
+            export.setFirstLevelContentPoolCount(categoryPoolCountMap.get(category));
+            export.setFirstLevelContentPoolDedupCount((long) categoryPoolTitleSetMap.get(category).size());
+            export.setReadCount(0L);
+            export.setPushFansCount(0L);
+            export.setPushReadCount(0L);
+            export.setPublishCount(0L);
+            categoryExportMap.put(category, export);
+        }
+
+        // 总量统计
+        long totalReadCount = 0L;
+        long totalPublishCount = 0L;
+        long totalFansCount = 0L;
+
+        for (DatastatSortStrategy item : firstLevelSortList) {
+            // 获取品类
+            String category = item.getCategory();
+            if (!categoryExportMap.containsKey(category)) {
+                continue;
+            }
+
+            LongArticlesSupplyAndDemandExport export = categoryExportMap.get(category);
+
+            // 累计发布条数
+            export.setPublishCount(export.getPublishCount() + 1);
+            totalPublishCount++;
+
+            // 阅读量
+            if (Objects.nonNull(item.getViewCount())) {
+                export.setReadCount(export.getReadCount() + item.getViewCount());
+                totalReadCount += item.getViewCount();
+            }
+
+            // 粉丝量
+            if (Objects.nonNull(item.getFans())) {
+                export.setPushFansCount(export.getPushFansCount() + item.getFans());
+                totalFansCount += item.getFans();
+            }
+
+            // push阅读量 = 首层阅读
+            if (Objects.nonNull(item.getFirstLevel())) {
+                export.setPushReadCount(export.getPushReadCount() + item.getFirstLevel());
+            }
+        }
+
+        // 9. 计算各项比率并设置分析字段
+        List<LongArticlesSupplyAndDemandExport> categoryResultList = new ArrayList<>();
+        for (String category : categoryList) {
+            LongArticlesSupplyAndDemandExport export = categoryExportMap.get(category);
+
+            // 头条内容池占比
+            if (totalFirstLevelContentPoolCount > 0) {
+                export.setFirstLevelContentPoolRate(export.getFirstLevelContentPoolCount() * 1.0 / totalFirstLevelContentPoolCount);
+            }
+
+            // 消费占比
+            if (totalReadCount > 0) {
+                export.setConsumptionRate(export.getReadCount() * 1.0 / totalReadCount);
+            }
+
+            // 发布条数占比
+            if (totalPublishCount > 0) {
+                export.setPublishCountRate(export.getPublishCount() * 1.0 / totalPublishCount);
+            }
+
+            // push粉丝量占比
+            if (totalFansCount > 0) {
+                export.setPushFansCountRate(export.getPushFansCount() * 1.0 / totalFansCount);
+            }
+
+            // push阅读率
+            if (Objects.nonNull(export.getPushFansCount()) && export.getPushFansCount() > 0) {
+                export.setPushReadRate(export.getPushReadCount() * 1.0 / export.getPushFansCount());
+            }
+
+            // 供需比 = 头条内容池占比/(消费占比+0.001)
+            if (Objects.nonNull(export.getFirstLevelContentPoolRate()) && Objects.nonNull(export.getConsumptionRate())) {
+                export.setSupplyDemandRate(export.getFirstLevelContentPoolRate() / (export.getConsumptionRate() + 0.001));
+            }
+
+            // 需曝比 = 消费占比/push粉丝量占比
+            if (Objects.nonNull(export.getConsumptionRate()) && Objects.nonNull(export.getPushFansCountRate()) && export.getPushFansCountRate() > 0) {
+                export.setDemandExposureRate(export.getConsumptionRate() / export.getPushFansCountRate());
+            }
+
+            // 供需分析
+            if (Objects.nonNull(export.getSupplyDemandRate())) {
+                if (export.getSupplyDemandRate() >= 1.1) {
+                    export.setSupplyDemandAnalysis("供给足");
+                } else if (export.getSupplyDemandRate() <= 0.9) {
+                    export.setSupplyDemandAnalysis("供给缺");
+                } else {
+                    export.setSupplyDemandAnalysis("供需平衡");
+                }
+            }
+
+            // push质量分析
+            if (Objects.nonNull(export.getPushReadRate())) {
+                double ratePercent = export.getPushReadRate() * 100;
+                if (ratePercent >= 0.75) {
+                    export.setPushQualityAnalysis("高质量");
+                } else if (ratePercent >= 0.5) {
+                    export.setPushQualityAnalysis("中质量");
+                } else {
+                    export.setPushQualityAnalysis("低质量");
+                }
+            }
+
+            // 供需举措
+            StringBuilder action = new StringBuilder();
+            if ("低质量".equals(export.getPushQualityAnalysis())) {
+                action.append("提质");
+            } else if ("高质量".equals(export.getPushQualityAnalysis())) {
+                action.append("保质");
+            }
+            if ("供给足".equals(export.getSupplyDemandAnalysis())) {
+                action.append("保持供给");
+            } else if ("供给缺".equals(export.getSupplyDemandAnalysis())) {
+                action.append("提供给");
+            }
+            export.setSupplyDemandAction(action.toString());
+
+            categoryResultList.add(export);
+        }
+
+        // 10. 构建SUM记录
+        LongArticlesSupplyAndDemandExport sumExport = buildSumExport(dateStr, categoryResultList);
+        sumExport.setFirstLevelContentPoolCount(totalFirstLevelContentPoolCount);
+        sumExport.setFirstLevelContentPoolDedupCount((long) totalPoolTitleSet.size());
+        sumExport.setReadCount(totalReadCount);
+        sumExport.setPushFansCount(totalFansCount);
+        sumExport.setPublishCount(totalPublishCount);
+        if (totalFansCount > 0) {
+            sumExport.setPushReadRate(sumExport.getPushReadCount() * 1.0 / totalFansCount);
+        }
+        // 需曝比 = 消费占比/push粉丝量占比,SUM记录都为1.0
+        sumExport.setDemandExposureRate(1.0);
+
+        result.addAll(categoryResultList);
+        result.add(sumExport);
+
+        return result;
+    }
+
+    private LongArticlesSupplyAndDemandExport buildSumExport(String dateStr, List<LongArticlesSupplyAndDemandExport> categoryResultList) {
+        LongArticlesSupplyAndDemandExport sumExport = new LongArticlesSupplyAndDemandExport();
+        sumExport.setDateStr(dateStr);
+        sumExport.setCategory("SUM");
+        sumExport.setFirstLevelContentPoolRate(1.0);
+        sumExport.setConsumptionRate(1.0);
+        sumExport.setSupplyDemandRate(1.0);
+        sumExport.setPublishCountRate(1.0);
+        sumExport.setPushFansCountRate(1.0);
+        sumExport.setPushReadCount(categoryResultList.stream()
+                .mapToLong(e -> Objects.nonNull(e.getPushReadCount()) ? e.getPushReadCount() : 0L).sum());
+
+        return sumExport;
+    }
 }
 }

+ 29 - 0
long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/util/feishu/FeiShu.java

@@ -49,6 +49,35 @@ public class FeiShu {
 
 
     }
     }
 
 
+    public static final String PQ_APPID = "cli_a51114cf8bf8d00c";
+    public static final String PQ_APPSECRET = "cNoTAqMpsAm7mPBcpCAXFfvOzCNL27fe";
+
+    public static Pair<String, Integer> requestPQAccessToken() {
+        Pair<String, Integer> result = Pair.of("", 0);
+        long startTime = System.currentTimeMillis();
+        int retryCount = 0;
+
+        while (retryCount < 3) {
+            try {
+                HashMap<String, String> params = new HashMap<>();
+                params.put("app_id", PQ_APPID);
+                params.put("app_secret", PQ_APPSECRET);
+                HttpResponseContent hrc = HttpClientUtils.postForm(URL_FEISHU_ACCESSTOKEN, params, new HashMap<>());
+                LOGGER.info("feishu accessToken response = {}", hrc.getBodyContent());
+                JSONObject response = JSONObject.parseObject(hrc.getBodyContent());
+                String accessToken = response.getString("tenant_access_token");
+                int expireSeconds = response.getIntValue("expire");
+                return Pair.of(accessToken, expireSeconds);
+            } catch (Exception e) {
+                LOGGER.error("requestFeishuAccessToken, error ,retryCount = {}, cost = {}", retryCount, System.currentTimeMillis() - startTime, e);
+            }
+            retryCount++;
+        }
+
+        return result;
+
+    }
+
     public static void main(String[] args) {
     public static void main(String[] args) {
         HashMap<String, String> params = new HashMap<>();
         HashMap<String, String> params = new HashMap<>();
         params.put("app_id", APPID);
         params.put("app_id", APPID);

+ 31 - 0
long-article-recommend-service/src/main/java/com/tzld/longarticle/recommend/server/util/feishu/FeishuMessageSender.java

@@ -83,6 +83,37 @@ public class FeishuMessageSender implements InitializingBean {
         }
         }
     }
     }
 
 
+    public void sendPQChat(String chatId, String title, Map<String, String> infos) {
+        sendPQChat(chatId, null, title, infos);
+    }
+
+    public void sendPQChat(String chatId, String userOpenId, String title, Map<String, String> infos) {
+        try {
+            Pair<String, Integer> token = FeiShu.requestPQAccessToken();
+            HashMap<String, String> header = new HashMap<>();
+            header.put("Authorization", "Bearer " + token.getFirst());
+            header.put("content-type", "application/json; charset=utf-8");
+
+            MessageConfig messageConfig = new MessageConfig();
+            MessageHeader messageHeader = new MessageHeader();
+            messageHeader.title = new HeadTitle(title);
+            List<BaseElement> elements = new ArrayList<>();
+            DivElement textElement = new DivElement();
+            for (Map.Entry<String, String> stringStringEntry : infos.entrySet()) {
+                if (TextUtils.isBlank(stringStringEntry.getValue())) {
+                    textElement.fields.add(new Filed(new MarkDownText(String.format("%s", stringStringEntry.getKey()))));
+                } else {
+                    textElement.fields.add(new Filed(new MarkDownText(String.format("**%s**:%s", stringStringEntry.getKey(), stringStringEntry.getValue()))));
+                }
+            }
+            elements.add(textElement);
+            MessageParams messageParams = new MessageParams(chatId, userOpenId, new Message(messageConfig, messageHeader, elements));
+            HttpResponseContent hrc = HttpClientUtils.postRequestBody(FeiShu.URL_FEISHU_SEND_MESSAGE, messageParams, header);
+        } catch (Exception e) {
+            LOGGER.error("FeishuMessageSender", e);
+        }
+    }
+
     public static void sendWebHookMessage(String robotId, String msg) {
     public static void sendWebHookMessage(String robotId, String msg) {
         // 使用自定义群机器人webhook方式,支持外部群
         // 使用自定义群机器人webhook方式,支持外部群
         if (!"prod".equals(staticEnv)) {
         if (!"prod".equals(staticEnv)) {

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

@@ -113,4 +113,11 @@ public class DataDashboardController {
         }).start();
         }).start();
     }
     }
 
 
+    @GetMapping("/export/contentSupplyAndDemandDashBoard")
+    public void contentSupplyAndDemandDashBoard(String dateStr) {
+        new Thread(() -> {
+            service.contentSupplyAndDemandDashBoard(dateStr);
+        }).start();
+    }
+
 }
 }

+ 14 - 2
long-article-recommend-service/src/main/resources/mapper/aigc/AigcBaseMapper.xml

@@ -32,11 +32,12 @@
         from produce_plan_exe_record record
         from produce_plan_exe_record record
          join produce_plan_module_output output
          join produce_plan_module_output output
           on record.plan_exe_id = output.plan_exe_id and output.produce_module_type = 3
           on record.plan_exe_id = output.plan_exe_id and output.produce_module_type = 3
-        where record.plan_id = #{planId} and record.status = 2 and audit_status in (1, 4, 5, 6)
+        where record.plan_id = #{planId} and record.status = 2 and audit_status in (1, 2, 4, 5, 6)
     </select>
     </select>
 
 
     <select id="getProduceContentByPlanExeIds" resultType="com.tzld.longarticle.recommend.server.model.dto.ProduceContentDTO">
     <select id="getProduceContentByPlanExeIds" resultType="com.tzld.longarticle.recommend.server.model.dto.ProduceContentDTO">
         select distinct record.channel_content_id as contentId,
         select distinct record.channel_content_id as contentId,
+                        record.plan_exe_id as planExeId,
                output.output             as title
                output.output             as title
         from produce_plan_exe_record record
         from produce_plan_exe_record record
          join produce_plan_module_output output
          join produce_plan_module_output output
@@ -45,7 +46,7 @@
         <foreach collection="producePlanExeIds" item="item" open="(" close=")" separator=",">
         <foreach collection="producePlanExeIds" item="item" open="(" close=")" separator=",">
             #{item}
             #{item}
         </foreach>
         </foreach>
-         and record.status = 2 and audit_status in (1, 4, 5, 6)
+         and record.status = 2 and audit_status in (1, 2, 4, 5, 6)
     </select>
     </select>
 
 
     <select id="getAllByProducePlanId"
     <select id="getAllByProducePlanId"
@@ -59,6 +60,17 @@
         and status = 2
         and status = 2
     </select>
     </select>
 
 
+    <select id="getAllPassContentByProducePlanId"
+            resultType="com.tzld.longarticle.recommend.server.model.entity.aigc.ProducePlanExeRecord">
+        select plan_exe_id, channel_content_id
+        from produce_plan_exe_record
+        where plan_id in
+        <foreach collection="producePlanIds" item="item" open="(" close=")" separator=",">
+            #{item}
+        </foreach>
+        and status = 2 and audit_status in (1, 3, 4, 5, 6)
+    </select>
+
     <select id="getCrawlerContentByChannelContentIdIn"
     <select id="getCrawlerContentByChannelContentIdIn"
             resultType="com.tzld.longarticle.recommend.server.model.dto.CrawlerContent">
             resultType="com.tzld.longarticle.recommend.server.model.dto.CrawlerContent">
         select cc.id, cc.channel_content_id, cprr.plan_id as crawlerPlanId, ca.wx_gh as ghId, cc.title, cc.publish_timestamp
         select cc.id, cc.channel_content_id, cprr.plan_id as crawlerPlanId, ca.wx_gh as ghId, cc.title, cc.publish_timestamp