wangyunpeng 1 miesiąc temu
rodzic
commit
5b5077f869

+ 30 - 0
core/src/main/java/com/tzld/supply/common/enums/SpiderContentScreenStatusEnum.java

@@ -0,0 +1,30 @@
+package com.tzld.supply.common.enums;
+
+import lombok.Getter;
+
+@Getter
+public enum SpiderContentScreenStatusEnum {
+    WAITING(0, "未分析"),
+    PASSED(1, "通过"),
+    ABANDONED(2, "废弃"),
+    ;
+
+    // 粗筛、精筛状态 0-未分析 1-通过 2-废弃
+    private int code;
+    private String msg;
+
+
+    SpiderContentScreenStatusEnum(int code, String msg) {
+        this.code = code;
+        this.msg = msg;
+    }
+
+    public static SpiderContentScreenStatusEnum getByCode(int code) {
+        for (SpiderContentScreenStatusEnum statusEnum : SpiderContentScreenStatusEnum.values()) {
+            if (statusEnum.getCode() == code) {
+                return statusEnum;
+            }
+        }
+        return null;
+    }
+}

+ 31 - 0
core/src/main/java/com/tzld/supply/common/enums/SpiderContentStatusEnum.java

@@ -0,0 +1,31 @@
+package com.tzld.supply.common.enums;
+
+import lombok.Getter;
+
+@Getter
+public enum SpiderContentStatusEnum {
+    WAITING(0, "待分析"),
+    FILTERING(1, "筛选中"),
+    PASSED(2, "通过"),
+    ABANDONED(3, "废弃"),
+    ;
+
+    // 处理状态 0-待分析 1-筛选中 2-通过 3-废弃
+    private int code;
+    private String msg;
+
+
+    SpiderContentStatusEnum(int code, String msg) {
+        this.code = code;
+        this.msg = msg;
+    }
+
+    public static SpiderContentStatusEnum getByCode(int code) {
+        for (SpiderContentStatusEnum statusEnum : SpiderContentStatusEnum.values()) {
+            if (statusEnum.getCode() == code) {
+                return statusEnum;
+            }
+        }
+        return null;
+    }
+}

+ 66 - 39
core/src/main/java/com/tzld/supply/job/ContentScreenJob.java

@@ -2,6 +2,8 @@ package com.tzld.supply.job;
 
 import com.alibaba.fastjson.JSONObject;
 import com.tzld.supply.api.DeepSeekApiService;
+import com.tzld.supply.common.enums.SpiderContentScreenStatusEnum;
+import com.tzld.supply.common.enums.SpiderContentStatusEnum;
 import com.tzld.supply.dao.mapper.supply.spider.SpiderContentMapper;
 import com.tzld.supply.dao.mapper.supply.spider.ext.SpiderMapperExt;
 import com.tzld.supply.model.entity.DeepSeekResult;
@@ -50,11 +52,11 @@ public class ContentScreenJob {
             if (Objects.nonNull(obj)) {
                 Integer aiRoughStatus = checkRoughScreenStatus(obj);
                 content.setAiRoughStatus(aiRoughStatus);
-                if (aiRoughStatus == 1) {
-                    content.setStatus(1);
+                if (aiRoughStatus == SpiderContentScreenStatusEnum.PASSED.getCode()) {
+                    content.setStatus(SpiderContentStatusEnum.FILTERING.getCode());
                     content.setAiRoughResult(JSONObject.toJSONString(obj));
                 } else {
-                    content.setStatus(3);
+                    content.setStatus(SpiderContentStatusEnum.ABANDONED.getCode());
                 }
                 content.setUpdateTime(System.currentTimeMillis());
                 spiderContentMapper.updateByPrimaryKeySelective(content);
@@ -66,32 +68,44 @@ public class ContentScreenJob {
 
     private Integer checkRoughScreenStatus(RoughScreenEntity obj) {
         if (Objects.isNull(obj.getIsElderlySuitable()) || !obj.getIsElderlySuitable()) {
-            return 2;
+            return SpiderContentScreenStatusEnum.ABANDONED.getCode();
         }
         if (Objects.nonNull(obj.getIsSensitive()) && obj.getIsSensitive()) {
-            return 2;
+            return SpiderContentScreenStatusEnum.ABANDONED.getCode();
         }
         if (Objects.nonNull(obj.getIsNegative()) && obj.getIsNegative()) {
-            return 2;
+            return SpiderContentScreenStatusEnum.ABANDONED.getCode();
         }
         if (Objects.nonNull(obj.getIsAdvertisement()) && obj.getIsAdvertisement()) {
-            return 2;
+            return SpiderContentScreenStatusEnum.ABANDONED.getCode();
         }
-        return 1;
+        return SpiderContentScreenStatusEnum.PASSED.getCode();
     }
 
-    private String roughScreenPrompt = "你是一名新闻智能审核系统的快速过滤模型,负责判断新闻标题是否适合老年群体内容推荐。\n" +
-            "\n" +
-            "【任务目标】\n" +
-            "根据输入的标题、来源,对新闻进行初步筛选。你的目标是判断内容是否:\n" +
-            "1. 涉及政治、暴力、色情、负面或争议;\n" +
-            "2. 属于广告或营销;\n" +
-            "3. 适合老年群体;\n" +
-            "4. 有一定正能量或实用性;\n" +
-            "5. 标题是否可进行正向优化(例如“更吸引但不夸大”)。\n" +
-            "\n" +
-            "【请严格按以下 JSON 结构输出,不要输出其他说明】:\n" +
+    private String roughScreenPrompt = "你是一名新闻智能审核系统的快速过滤模型,负责判断新闻标题是否适合老年群体内容推荐。请根据输入的标题和来源,对新闻进行初步筛选,并输出结构化结果。\n" +
+            "任务目标:\n" +
+            "判断是否涉及政治、暴力、色情、负面或争议:\n" +
+            " 政治类包括政府决策、领导人言论、外交、政策宣传等;\n" +
+            " 暴力类包括打斗、战争、犯罪、事故、血腥场景等;\n" +
+            " 色情类包括涉及性暗示、低俗内容、娱乐绯闻等;\n" +
+            " 负面类包括疾病、灾难、焦虑、冲突、悲剧等;\n" +
+            " 争议类包括社会矛盾、热点舆论事件、群体对立等。\n" +
+            "判断是否属于广告或营销:\n" +
+            " 包含品牌名、产品、促销、购买引导、价格描述、联系方式、二维码等;\n" +
+            " 内容带有明显推广意图或商业导向。\n" +
+            "判断是否适合老年群体:\n" +
+            " 内容主题应偏向健康、养生、生活常识、家庭关系、历史文化、节日习俗等;\n" +
+            " 避免复杂科技、游戏、金融、投资、网络用语等老年人不易理解的内容;\n" +
+            " 表达方式应平实简洁,信息有实用价值或积极意义。\n" +
+            "判断是否具有正能量或实用性:\n" +
+            " 正能量内容包括积极向上、健康生活、社会关怀、家庭和谐等主题;\n" +
+            " 实用内容包括健康建议、生活窍门、养生技巧、节气知识、天气预报、历史科普等;\n" +
+            " 对老年人有帮助的内容应优先保留。\n" +
+            "判断标题是否可进行正向优化(更吸引但不夸大):\n" +
+            " 若标题内容平淡,但可通过增加情感词汇或轻度修饰(如“实用技巧”、“医生建议”、“家家都该知道”等)提升吸引力,则可标记为可优化;\n" +
+            " 不得改变原文事实或引导误解,不得使用虚假或夸张性描述。\n" +
             "\n" +
+            "请严格按以下 JSON 格式输出,不要输出任何说明或注释:\n" +
             "{\n" +
             "  \"is_elderly_suitable\": true/false,\n" +
             "  \"is_sensitive\": true/false,\n" +
@@ -134,13 +148,13 @@ public class ContentScreenJob {
             if (Objects.nonNull(obj)) {
                 Integer aiPrecisionStatus = checkPrecisionScreenStatus(obj);
                 content.setAiPrecisionStatus(aiPrecisionStatus);
-                if (aiPrecisionStatus == 1
+                if (aiPrecisionStatus == SpiderContentScreenStatusEnum.PASSED.getCode()
                         && obj.getPropagationScore() >= 0.8
                         && obj.getSuitabilityScore() >= 0.8) {
-                    content.setStatus(2);
+                    content.setStatus(SpiderContentStatusEnum.PASSED.getCode());
                     content.setAiPrecisionResult(JSONObject.toJSONString(obj));
                 } else {
-                    content.setStatus(3);
+                    content.setStatus(SpiderContentStatusEnum.ABANDONED.getCode());
                 }
                 content.setUpdateTime(System.currentTimeMillis());
                 spiderContentMapper.updateByPrimaryKeySelective(content);
@@ -151,34 +165,47 @@ public class ContentScreenJob {
 
     private Integer checkPrecisionScreenStatus(PrecisionScreenEntity obj) {
         if (Objects.isNull(obj.getIsElderlySuitable()) || !obj.getIsElderlySuitable()) {
-            return 2;
+            return SpiderContentScreenStatusEnum.ABANDONED.getCode();
         }
         if (Objects.nonNull(obj.getIsSensitive()) && obj.getIsSensitive()) {
-            return 2;
+            return SpiderContentScreenStatusEnum.ABANDONED.getCode();
         }
         if (Objects.nonNull(obj.getIsNegative()) && obj.getIsNegative()) {
-            return 2;
+            return SpiderContentScreenStatusEnum.ABANDONED.getCode();
         }
         if (Objects.nonNull(obj.getIsAdvertisement()) && obj.getIsAdvertisement()) {
-            return 2;
+            return SpiderContentScreenStatusEnum.ABANDONED.getCode();
         }
-        return 1;
+        return SpiderContentScreenStatusEnum.PASSED.getCode();
     }
 
-    private String precisionScreenPrompt = "你是一名内容智能分析与编辑顾问,负责对新闻或视频进行全面分析。\n" +
-            "请根据以下内容,判断其是否适合老年群体推荐,并输出结构化结果。\n" +
-            "\n" +
-            "【分析目标】\n" +
-            "1. 判断是否适合老年人阅读;\n" +
-            "2. 判断是否包含政治、敏感、广告或负面内容;\n" +
-            "3. 判断内容主题、地域、关键词;\n" +
-            "4. 判断是否可以在不误导、不造假的前提下适度夸大或优化标题;\n" +
-            "5. 提取内容亮点与传播潜力;\n" +
-            "6. 给出传播评分和推荐评分;\n" +
-            "7. 若适合改写,请输出更吸引人的标题。\n" +
+    private String precisionScreenPrompt = "你是一名内容智能分析与编辑顾问,负责对新闻或视频进行全面分析。请根据以下内容,判断其是否适合老年群体推荐,并输出结构化结果。\n" +
             "\n" +
-            "【请严格按以下 JSON 格式输出】:\n" +
+            "分析目标:\n" +
+            "1. 判断是否适合老年人阅读:\n" +
+            "   内容应具备健康、生活、历史、天气、节假日、社会正能量等特征;避免涉及复杂科技、互联网、游戏、金融投机等难以理解或风险性内容。\n" +
+            "   语言表达应平实易懂,不含网络黑话、缩写或复杂术语。\n" +
+            "2. 判断是否包含政治、敏感、广告或负面内容:\n" +
+            "   政治:涉及国家政策、领导人、外交等信息;\n" +
+            "   敏感:涉及宗教、种族、灾难、社会冲突等;\n" +
+            "   负面:传播焦虑、恐慌、暴力、疾病死亡等信息;\n" +
+            "   广告:带有明显推广目的,如品牌、价格、产品功能描述等。\n" +
+            "3. 判断内容主题、地域、关键词:\n" +
+            "   主题:健康、生活、历史、天气、节假日、军事、社会、其他;\n" +
+            "   地域:提取与城市、省份或国家相关的地域信息;\n" +
+            "   关键词:提取3~5个能代表内容核心的热词。\n" +
+            "4. 判断是否可以在不误导、不造假的前提下适度夸大或优化标题:\n" +
+            "   夸大标准包括:是否能使用更具吸引力的词汇(如“意想不到”、“太实用了”、“医生都建议”、“家家都能用上”等),但不能改变事实或造成误导。\n" +
+            "5. 提取内容亮点与传播潜力:\n" +
+            "   内容亮点:是否包含实用技巧、经验分享、生活智慧或积极故事;\n" +
+            "   传播潜力:判断是否具备高关注度话题、共鸣性或视觉吸引力(如图片、视频内容)。\n" +
+            "6. 给出传播评分和推荐评分:\n" +
+            "   传播评分(0.0~1.0):根据内容话题性、情绪感染力、关键词热度等评估;\n" +
+            "   推荐评分(0.0~1.0):根据内容积极性、老年适配度、价值导向评估。\n" +
+            "7. 若适合改写,请输出更吸引人的标题:\n" +
+            "   若标题平淡但内容有亮点,可提出优化建议或改写方案,使其更具吸引力和传播性。\n" +
             "\n" +
+            "请严格按以下 JSON 格式输出,不要输出任何解释说明:\n" +
             "{\n" +
             "  \"is_elderly_suitable\": true/false,\n" +
             "  \"is_sensitive\": true/false,\n" +

+ 114 - 0
core/src/main/java/com/tzld/supply/job/SpiderContentFeishuJob.java

@@ -0,0 +1,114 @@
+package com.tzld.supply.job;
+
+import cn.hutool.core.collection.CollectionUtil;
+import com.alibaba.fastjson.JSONObject;
+import com.google.common.collect.Lists;
+import com.tzld.supply.common.enums.SpiderContentStatusEnum;
+import com.tzld.supply.dao.mapper.supply.spider.SpiderContentMapper;
+import com.tzld.supply.model.entity.PrecisionScreenEntity;
+import com.tzld.supply.model.entity.feishu.SpiderContentEntity;
+import com.tzld.supply.model.po.supply.spider.SpiderContent;
+import com.tzld.supply.model.po.supply.spider.SpiderContentExample;
+import com.tzld.supply.util.feishu.FeiShu;
+import com.tzld.supply.util.feishu.FeishuExcelUtil;
+import com.xxl.job.core.biz.model.ReturnT;
+import com.xxl.job.core.handler.annotation.XxlJob;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.util.Pair;
+import org.springframework.http.HttpHeaders;
+import org.springframework.stereotype.Component;
+import org.springframework.web.client.RestTemplate;
+
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.List;
+
+@Slf4j
+@Component
+public class SpiderContentFeishuJob {
+
+    @Autowired
+    private SpiderContentMapper spiderContentMapper;
+
+
+    @XxlJob("spiderContentFeishuJob")
+    public ReturnT<String> spiderContentFeishuJob(String param) {
+        SpiderContentExample example = new SpiderContentExample();
+        example.createCriteria().andStatusIn(Lists.newArrayList(2, 3)).andAiRoughStatusEqualTo(1);
+        List<SpiderContent> dataList = spiderContentMapper.selectByExampleWithBLOBs(example);
+        if (CollectionUtil.isEmpty(dataList)) {
+            return ReturnT.SUCCESS;
+        }
+        List<SpiderContentEntity> exportList = new ArrayList<>();
+        for (SpiderContent spiderContent : dataList) {
+            SpiderContentEntity spiderContentEntity = new SpiderContentEntity();
+            spiderContentEntity.setSource(spiderContent.getSource() + "-" + spiderContent.getSourceType());
+            spiderContentEntity.setTitle(spiderContent.getTitle());
+            spiderContentEntity.setStatus(SpiderContentStatusEnum.getByCode(spiderContent.getStatus()).getMsg());
+            if (StringUtils.isNotBlank(spiderContent.getAiPrecisionResult())) {
+                PrecisionScreenEntity entity = JSONObject.parseObject(spiderContent.getAiPrecisionResult(), PrecisionScreenEntity.class);
+                spiderContentEntity.setImprovedTitle(entity.getImprovedTitle());
+            }
+            spiderContentEntity.setContent(spiderContent.getContent());
+            exportList.add(spiderContentEntity);
+        }
+        spiderContentFeishuExport(exportList);
+        return ReturnT.SUCCESS;
+    }
+
+    public void spiderContentFeishuExport(List<SpiderContentEntity> exportList) {
+        if (CollectionUtil.isEmpty(exportList)) {
+            return;
+        }
+        int rowNum = exportList.size();
+        List<List<Object>> rows = new ArrayList<>();
+        Field[] fields = SpiderContentEntity.class.getDeclaredFields();
+        for (SpiderContentEntity 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());
+                }
+            }
+        }
+
+        doSendFeishuSheet("RKgdwQsUbix4lfkOtwKcJ8DonDh", "a337ce", rowNum, rows,
+                2, null, null);
+    }
+
+    public void doSendFeishuSheet(String sheetToken, String sheetId,
+                                  int rowNum, List<List<Object>> rows, Integer startRowIndex,
+                                  List<Pair<String, String>> styles,
+                                  List<Pair<String, List<Pair<String, String>>>> thanks) {
+        Pair<String, Integer> token = FeiShu.requestAccessToken();
+        RestTemplate restTemplate = new RestTemplate();
+        HttpHeaders httpHeaders = new HttpHeaders();
+        httpHeaders.add("Authorization", "Bearer " + token.getFirst());
+
+        // 先删除掉已存在的dateStr数据
+        // FeishuExcelUtil.feishuSheetDelete(sheetToken, sheetId, rowNum, startRowIndex, httpHeaders, restTemplate);
+        List<List<List<Object>>> partitions = Lists.partition(rows, 100);
+        FeishuExcelUtil.feishuSheetInsert(sheetToken, sheetId, startRowIndex, httpHeaders, restTemplate, partitions);
+        // 此处先简单处理,调整单元格为”百分比小数点“
+        try {
+            Thread.sleep(1000);
+        } catch (Exception e) {
+            log.error("export sleep error");
+        }
+        if (CollectionUtil.isNotEmpty(styles)) {
+            FeishuExcelUtil.feishuSheetStyle(sheetToken, sheetId, rowNum, startRowIndex, httpHeaders, restTemplate, styles);
+        }
+        if (CollectionUtil.isNotEmpty(thanks)) {
+            FeishuExcelUtil.feishuSheetThanks(sheetToken, sheetId, rowNum, startRowIndex, httpHeaders, restTemplate, thanks);
+        }
+    }
+}

+ 2 - 1
core/src/main/java/com/tzld/supply/job/SpiderJob.java

@@ -1,6 +1,7 @@
 package com.tzld.supply.job;
 
 import com.tzld.supply.api.SpiderApiService;
+import com.tzld.supply.common.enums.SpiderContentStatusEnum;
 import com.tzld.supply.dao.mapper.supply.spider.SpiderContentMapper;
 import com.tzld.supply.dao.mapper.supply.spider.SpiderTaskMapper;
 import com.tzld.supply.dao.mapper.supply.spider.ext.SpiderMapperExt;
@@ -131,7 +132,7 @@ public class SpiderJob {
             if (StringUtils.isNotBlank(text)) {
                 content.setContent(text);
             } else {
-                content.setStatus(3);
+                content.setStatus(SpiderContentStatusEnum.ABANDONED.getCode());
             }
             content.setUpdateTime(System.currentTimeMillis());
             spiderContentMapper.updateByPrimaryKeySelective(content);

+ 12 - 0
core/src/main/java/com/tzld/supply/model/entity/feishu/SpiderContentEntity.java

@@ -0,0 +1,12 @@
+package com.tzld.supply.model.entity.feishu;
+
+import lombok.Data;
+
+@Data
+public class SpiderContentEntity {
+    private String source;
+    private String title;
+    private String status;
+    private String improvedTitle;
+    private String content;
+}

+ 177 - 0
core/src/main/java/com/tzld/supply/util/feishu/FeishuExcelUtil.java

@@ -0,0 +1,177 @@
+package com.tzld.supply.util.feishu;
+
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.tzld.supply.util.MapBuilder;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.util.Pair;
+import org.springframework.http.*;
+import org.springframework.web.client.RestTemplate;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+@Slf4j
+public class FeishuExcelUtil {
+
+    public static void feishuSheetDelete(String sheetToken,
+                                   String sheetId,
+                                   int rowNum,
+                                   int startRowIndex,
+                                   HttpHeaders httpHeaders,
+                                   RestTemplate restTemplate,
+                                   List<String> dateStrList) {
+        HttpEntity<Object> queryEntity = new HttpEntity<>(httpHeaders);
+        int deleteRowNum = rowNum < 20 ? startRowIndex + rowNum : startRowIndex + (rowNum * 2);
+        ResponseEntity<String> queryResponseEntity = restTemplate.exchange(
+                String.format("https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/%s/values/%s!A"
+                        + startRowIndex + ":A" + deleteRowNum, sheetToken, sheetId),
+                HttpMethod.GET, queryEntity, String.class);
+        JSONArray values = JSONObject.parseObject(queryResponseEntity.getBody())
+                .getJSONObject("data")
+                .getJSONObject("valueRange")
+                .getJSONArray("values");
+        int count = 0;
+        if (!values.isEmpty() && Objects.nonNull(values.get(0)) && !((JSONArray) values.get(0)).isEmpty()
+                && Objects.nonNull(((JSONArray) values.get(0)).get(0))
+                && dateStrList.contains(((JSONArray) values.get(0)).get(0).toString())) {
+            for (Object value : values) {
+                if (((JSONArray) value).get(0) != null) {
+                    List<String> dates = ((JSONArray) value).stream().map(Object::toString).collect(Collectors.toList());
+                    if (dateStrList.contains(dates.get(0))) {
+                        count++;
+                        continue;
+                    }
+                }
+                break;
+            }
+        }
+        if (count > 0) {
+            int delNum = 0;
+            do {
+                // 删除当前日期已存在的旧数据
+                httpHeaders.setContentType(MediaType.APPLICATION_JSON);
+                HttpEntity<Object> deleteEntity = new HttpEntity<>(
+                        String.format("{\n" +
+                                "    \"dimension\": {\n" +
+                                "        \"sheetId\": \"%s\",\n" +
+                                "        \"majorDimension\": \"ROWS\",\n" +
+                                "        \"startIndex\": %s,\n" +
+                                "        \"endIndex\": %s\n" +
+                                "    }\n" +
+                                "}", sheetId, startRowIndex, Math.min(startRowIndex + 4000, count - delNum + startRowIndex) - 1),
+                        httpHeaders);
+                restTemplate.exchange(String.format("https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/%s/dimension_range", sheetToken),
+                        HttpMethod.DELETE, deleteEntity, String.class);
+                delNum = Math.min(delNum + 4000, count);
+            } while (delNum < count);
+        }
+    }
+
+    public static void feishuSheetInsert(String sheetToken,
+                                   String sheetId,
+                                   Integer startRowIndex,
+                                   HttpHeaders httpHeaders,
+                                   RestTemplate restTemplate,
+                                   List<List<List<Object>>> partitions) {
+        int startRow = startRowIndex;
+        for (List<List<Object>> partition : partitions) {
+            // 插入数据
+            HttpEntity<Object> postEntity = new HttpEntity<>(MapBuilder
+                    .builder()
+                    .put("valueRange", MapBuilder
+                            .builder()
+                            .put("range", String.format("%s!A" + startRow + ":CI", sheetId) + (partition.size() + startRow - 1))
+                            .put("values", partition)
+                            .build())
+                    .build(), httpHeaders);
+            ResponseEntity<String> response = restTemplate.exchange(String.format("https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/%s/values_prepend",
+                            sheetToken),
+                    HttpMethod.POST, postEntity, String.class);
+            JSONObject responseJSON = JSONObject.parseObject(response.getBody());
+            if (0 != responseJSON.getInteger("code")) {
+                log.error("doSendFeishuSheet write error :{}", responseJSON.getString("msg"));
+            }
+            startRow += partition.size();
+        }
+    }
+
+    public static void feishuSheetStyle(String sheetToken,
+                                  String sheetId,
+                                  int rowNum,
+                                  Integer startRowIndex,
+                                  HttpHeaders httpHeaders,
+                                  RestTemplate restTemplate,
+                                  List<Pair<String, String>> styles) {
+        Integer startRow = startRowIndex;
+        do {
+            for (Pair<String, String> style : styles) {
+                HttpEntity<Map<Object, Object>> styleEntity = new HttpEntity<>(MapBuilder
+                        .builder()
+                        .put("appendStyle",
+                                MapBuilder
+                                        .builder()
+                                        .put("range", String.format("%s!%s" + startRow + ":%s", sheetId,
+                                                style.getFirst(), style.getFirst())
+                                                + (Math.min(startRow + 4000, rowNum + startRowIndex) - 1))
+                                        .put("style",
+                                                MapBuilder
+                                                        .builder()
+                                                        .put("formatter", style.getSecond())
+                                                        .build()
+                                        )
+                                        .build()
+                        )
+                        .build(), httpHeaders);
+                restTemplate.exchange(
+                        String.format("https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/%s/style",
+                                sheetToken),
+                        HttpMethod.PUT,
+                        styleEntity,
+                        String.class
+                );
+            }
+            startRow += 4000;
+        } while (startRow < rowNum);
+    }
+
+    public static void feishuSheetThanks(String sheetToken,
+                                   String sheetId,
+                                   int rowNum,
+                                   Integer startRowIndex,
+                                   HttpHeaders httpHeaders,
+                                   RestTemplate restTemplate,
+                                   List<Pair<String, List<Pair<String, String>>>> thanks) {
+        Integer startRow = startRowIndex;
+        do {
+            for (Pair<String, List<Pair<String, String>>> thank : thanks) {
+                List<String> keyList = thank.getSecond().stream().map(Pair::getFirst).collect(Collectors.toList());
+                List<String> colorList = thank.getSecond().stream().map(Pair::getSecond).collect(Collectors.toList());
+                HttpEntity<Map<Object, Object>> styleEntity = new HttpEntity<>(MapBuilder
+                        .builder()
+                        .put("range", String.format("%s!%s" + startRow + ":%s", sheetId,
+                                thank.getFirst(), thank.getFirst())
+                                + (Math.min(startRow + 4000, rowNum + startRowIndex) - 1))
+                        .put("dataValidationType", "list")
+                        .put("dataValidation", MapBuilder.builder()
+                                .put("conditionValues", keyList)
+                                .put("options", MapBuilder.builder()
+                                        .put("highlightValidData", true)
+                                        .put("colors", colorList)
+                                        .build()
+                                ).build())
+                        .build(), httpHeaders);
+                restTemplate.exchange(
+                        String.format("https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/%s/dataValidation",
+                                sheetToken),
+                        HttpMethod.POST,
+                        styleEntity,
+                        String.class
+                );
+            }
+            startRow += 4000;
+        } while (startRow < rowNum);
+    }
+}

+ 9 - 0
server/src/main/java/com/tzld/supply/controller/XxlJobController.java

@@ -2,6 +2,7 @@ package com.tzld.supply.controller;
 
 import com.tzld.supply.common.base.CommonResponse;
 import com.tzld.supply.job.ContentScreenJob;
+import com.tzld.supply.job.SpiderContentFeishuJob;
 import com.tzld.supply.job.SpiderJob;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.GetMapping;
@@ -16,6 +17,8 @@ public class XxlJobController {
     private SpiderJob spiderJob;
     @Autowired
     private ContentScreenJob contentScreenJob;
+    @Autowired
+    private SpiderContentFeishuJob contentFeishuJob;
 
     @GetMapping("/spiderTaskJob")
     public CommonResponse<Void> spiderTaskJob() {
@@ -40,4 +43,10 @@ public class XxlJobController {
         contentScreenJob.contentPrecisionScreenJob(null);
         return CommonResponse.success();
     }
+
+    @GetMapping("/spiderContentFeishuJob")
+    public CommonResponse<Void> spiderContentFeishuJob() {
+        contentFeishuJob.spiderContentFeishuJob(null);
+        return CommonResponse.success();
+    }
 }