Sfoglia il codice sorgente

场景已看视频、静态变量枚举类

wangyunpeng 7 ore fa
parent
commit
4b71422e7a
17 ha cambiato i file con 473 aggiunte e 157 eliminazioni
  1. 19 0
      core/src/main/java/com/tzld/videoVector/common/constant/VectorConstants.java
  2. 25 0
      core/src/main/java/com/tzld/videoVector/common/enums/ContentTypeEnum.java
  3. 50 0
      core/src/main/java/com/tzld/videoVector/common/enums/DeconstructStatusEnum.java
  4. 64 0
      core/src/main/java/com/tzld/videoVector/common/enums/DimensionEnum.java
  5. 25 0
      core/src/main/java/com/tzld/videoVector/common/enums/MatchMethodEnum.java
  6. 70 0
      core/src/main/java/com/tzld/videoVector/common/enums/PointTypeEnum.java
  7. 39 0
      core/src/main/java/com/tzld/videoVector/common/enums/SourceTypeEnum.java
  8. 2 5
      core/src/main/java/com/tzld/videoVector/job/ArticleTextVectorJob.java
  9. 6 8
      core/src/main/java/com/tzld/videoVector/job/ArticleVectorJob.java
  10. 95 60
      core/src/main/java/com/tzld/videoVector/job/ChannelDemandMatchJob.java
  11. 5 3
      core/src/main/java/com/tzld/videoVector/job/MaterialDeconstructCheckJob.java
  12. 6 8
      core/src/main/java/com/tzld/videoVector/job/MaterialVectorJob.java
  13. 11 9
      core/src/main/java/com/tzld/videoVector/job/VideoVectorJob.java
  14. 3 1
      core/src/main/java/com/tzld/videoVector/service/impl/DeconstructServiceImpl.java
  15. 17 12
      core/src/main/java/com/tzld/videoVector/service/impl/MaterialSearchServiceImpl.java
  16. 17 17
      core/src/main/java/com/tzld/videoVector/service/impl/VideoSearchServiceImpl.java
  17. 19 34
      core/src/main/java/com/tzld/videoVector/service/recall/impl/VectorRecallTestServiceImpl.java

+ 19 - 0
core/src/main/java/com/tzld/videoVector/common/constant/VectorConstants.java

@@ -8,6 +8,14 @@ import java.util.List;
  */
 public interface VectorConstants {
 
+    // ========================== 解构数据来源 ==========================
+
+    /** AIGC 解构数据来源标识 */
+    String SOURCE_AIGC = "aigc_deconstruct";
+
+    /** ODPS 文本数据来源标识 */
+    String SOURCE_ODPS_TEXT = "odps_text";
+
     // ========================== 配置编码 ==========================
 
     /** 默认配置编码(选题) */
@@ -76,6 +84,17 @@ public interface VectorConstants {
     /** ODPS SQL IN 子句单批最大 ID 数(防止 SQL 超长) */
     int ODPS_IN_BATCH_SIZE = 1000;
 
+    // ========================== 渠道需求匹配 Redis Key ==========================
+
+    /** 渠道需求-向量召回结果缓存前缀 */
+    String CHANNEL_DEMAND_RECALL_CACHE_PREFIX = "channel_demand:recall:";
+
+    /** 渠道需求-Library API 召回结果缓存前缀 */
+    String CHANNEL_DEMAND_LIBRARY_RECALL_CACHE_PREFIX = "channel_demand:library_recall:";
+
+    /** 渠道需求-召回结果缓存过期时间(秒) */
+    long CHANNEL_DEMAND_RECALL_CACHE_EXPIRE = 6 * 60 * 60;
+
     // ========================== 召回参数 ==========================
 
     /**

+ 25 - 0
core/src/main/java/com/tzld/videoVector/common/enums/ContentTypeEnum.java

@@ -0,0 +1,25 @@
+package com.tzld.videoVector.common.enums;
+
+/**
+ * 内容类型枚举,用于解构内容区分
+ */
+public enum ContentTypeEnum {
+
+    /** 长文/文章 */
+    ARTICLE((short) 1),
+    /** 图文 */
+    IMAGE_TEXT((short) 2),
+    /** 视频 */
+    VIDEO((short) 3),
+    ;
+
+    private final short code;
+
+    ContentTypeEnum(short code) {
+        this.code = code;
+    }
+
+    public short getCode() {
+        return code;
+    }
+}

+ 50 - 0
core/src/main/java/com/tzld/videoVector/common/enums/DeconstructStatusEnum.java

@@ -0,0 +1,50 @@
+package com.tzld.videoVector.common.enums;
+
+/**
+ * 解构任务状态枚举
+ * <p>PENDING=提交但未开始, RUNNING=处理中, SUCCESS=成功, FAILED=失败
+ */
+public enum DeconstructStatusEnum {
+
+    PENDING((short) 0, "PENDING"),
+    RUNNING((short) 1, "RUNNING"),
+    SUCCESS((short) 2, "SUCCESS"),
+    FAILED((short) 3, "FAILED"),
+    ;
+
+    private final short code;
+    private final String label;
+
+    DeconstructStatusEnum(short code, String label) {
+        this.code = code;
+        this.label = label;
+    }
+
+    public short getCode() {
+        return code;
+    }
+
+    public String getLabel() {
+        return label;
+    }
+
+    /** 是否为终态(成功或失败) */
+    public boolean isTerminal() {
+        return this == SUCCESS || this == FAILED;
+    }
+
+    /** 判断 status 对应的终态 */
+    public static boolean isTerminalCode(Short status) {
+        return matchCode(status, SUCCESS) || matchCode(status, FAILED);
+    }
+
+    /** 根据数值 code 判断是否匹配 */
+    public static boolean matchCode(Short status, DeconstructStatusEnum target) {
+        return status != null && status == target.code;
+    }
+
+    /** 判断状态码是否为非失败态(可视为仍有效的结果) */
+    public static boolean isNotFailed(Short status) {
+        return status != null && status != FAILED.code;
+    }
+}

+ 64 - 0
core/src/main/java/com/tzld/videoVector/common/enums/DimensionEnum.java

@@ -0,0 +1,64 @@
+package com.tzld.videoVector.common.enums;
+
+import java.util.Arrays;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * 渠道驱动维度_空间 枚举
+ */
+public enum DimensionEnum {
+
+    /** 传播的头部 */
+    SPREAD_HEAD("传播的头部", DimensionGroup.HEAD),
+    /** 增长的头部 */
+    GROWTH_HEAD("增长的头部", DimensionGroup.HEAD),
+    /** 传播的分发 */
+    SPREAD_DISTRIBUTION("传播的分发", DimensionGroup.DISTRIBUTION),
+    /** 增长的分发 */
+    GROWTH_DISTRIBUTION("增长的分发", DimensionGroup.DISTRIBUTION),
+    ;
+
+    /**
+     * 维度分组:头部 / 分发
+     */
+    public enum DimensionGroup {
+        HEAD, DISTRIBUTION
+    }
+
+    private final String value;
+    private final DimensionGroup group;
+
+    DimensionEnum(String value, DimensionGroup group) {
+        this.value = value;
+        this.group = group;
+    }
+
+    public String getValue() {
+        return value;
+    }
+
+    public DimensionGroup getGroup() {
+        return group;
+    }
+
+    /** 是否为分发维度 */
+    public boolean isDistribution() {
+        return group == DimensionGroup.DISTRIBUTION;
+    }
+
+    /** 所有维度值(逗号拼接,用于 SQL IN 子句) */
+    public static String toSqlInValues() {
+        return Arrays.stream(values())
+                .map(d -> "'" + d.getValue() + "'")
+                .collect(Collectors.joining(", "));
+    }
+
+    /** 所有分发维度值的 Set */
+    public static Set<String> distributionValueSet() {
+        return Arrays.stream(values())
+                .filter(DimensionEnum::isDistribution)
+                .map(DimensionEnum::getValue)
+                .collect(Collectors.toSet());
+    }
+}

+ 25 - 0
core/src/main/java/com/tzld/videoVector/common/enums/MatchMethodEnum.java

@@ -0,0 +1,25 @@
+package com.tzld.videoVector.common.enums;
+
+/**
+ * 匹配手段枚举
+ */
+public enum MatchMethodEnum {
+
+    /** 场景已看视频(来自维度统计表,不走向量召回) */
+    DIMENSION_STAT("场景已看视频"),
+    /** 视频库_解构特征_向量相似匹配(走向量召回) */
+    RECOMMEND_LIB("视频库_解构特征_向量相似匹配"),
+    /** 视频库_解构特征点_精准匹配(Library API 精准匹配) */
+    RECOMMEND_LIB_EXACT("视频库_解构特征点_精准匹配"),
+    ;
+
+    private final String value;
+
+    MatchMethodEnum(String value) {
+        this.value = value;
+    }
+
+    public String getValue() {
+        return value;
+    }
+}

+ 70 - 0
core/src/main/java/com/tzld/videoVector/common/enums/PointTypeEnum.java

@@ -0,0 +1,70 @@
+package com.tzld.videoVector.common.enums;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * 需求特征点类型枚举,关联对应的向量配置编码列表
+ */
+public enum PointTypeEnum {
+
+    /** 关键点 */
+    KEY_POINT("关键点", "VIDEO_KEYPOINT", "KEYPOINT_SUBSTANCE", "KEYPOINT_FORM"),
+    /** 灵感点 */
+    INSPIRATION("灵感点", "VIDEO_INSPIRATION", "INSPIRATION_SUBSTANCE", "INSPIRATION_FORM"),
+    /** 目的点 */
+    PURPOSE("目的点", "VIDEO_PURPOSE", "PURPOSE_SUBSTANCE", "PURPOSE_FORM"),
+    ;
+
+    private final String value;
+    private final List<String> configCodes;
+
+    PointTypeEnum(String value, String... configCodes) {
+        this.value = value;
+        this.configCodes = Arrays.asList(configCodes);
+    }
+
+    public String getValue() {
+        return value;
+    }
+
+    public List<String> getConfigCodes() {
+        return configCodes;
+    }
+
+    /** 默认配置编码列表(用于未知 pointType 的兜底) */
+    private static final List<String> DEFAULT_CONFIG_CODES = Arrays.asList("VIDEO_TOPIC");
+
+    /** 根据 pointType 字符串值获取对应的配置编码列表,未匹配时返回默认值 */
+    public static List<String> getConfigCodesByValue(String pointType) {
+        for (PointTypeEnum e : values()) {
+            if (e.value.equals(pointType)) {
+                return e.getConfigCodes();
+            }
+        }
+        return DEFAULT_CONFIG_CODES;
+    }
+
+    /** 所有 pointType 字符串值的列表(顺序:灵感点, 关键点, 目的点,与解构解析顺序一致) */
+    public static List<String> getAllValueList() {
+        return Collections.unmodifiableList(
+                Arrays.asList(INSPIRATION.value, KEY_POINT.value, PURPOSE.value));
+    }
+
+    /** 按 type 取值的预初始化 Map 键集合,用于分桶聚合 */
+    public static Map<String, List<String>> emptyTypeNameMap() {
+        return getAllValueList().stream()
+                .collect(Collectors.toMap(t -> t, t -> new ArrayList<>(), (a, b) -> a, LinkedHashMap::new));
+    }
+
+    /** 按 type 取值的预初始化 Map 键集合(essence 类型) */
+    public static Map<String, List<Map<String, Object>>> emptyTypeEssenceMap() {
+        return getAllValueList().stream()
+                .collect(Collectors.toMap(t -> t, t -> new ArrayList<>(), (a, b) -> a, LinkedHashMap::new));
+    }
+}

+ 39 - 0
core/src/main/java/com/tzld/videoVector/common/enums/SourceTypeEnum.java

@@ -0,0 +1,39 @@
+package com.tzld.videoVector.common.enums;
+
+/**
+ * 素材来源类型枚举
+ */
+public enum SourceTypeEnum {
+
+    /** 外部合作 */
+    EXTERNAL((short) 1, "外部合作"),
+    /** 内部素材 */
+    INTERNAL((short) 2, "内部素材"),
+    ;
+
+    private final short code;
+    private final String label;
+
+    SourceTypeEnum(short code, String label) {
+        this.code = code;
+        this.label = label;
+    }
+
+    public short getCode() {
+        return code;
+    }
+
+    public String getLabel() {
+        return label;
+    }
+
+    /** 根据来源标签获取 code,未匹配返回 null */
+    public static Short getCodeByLabel(String label) {
+        for (SourceTypeEnum e : values()) {
+            if (e.label.equals(label)) {
+                return e.code;
+            }
+        }
+        return null;
+    }
+}

+ 2 - 5
core/src/main/java/com/tzld/videoVector/job/ArticleTextVectorJob.java

@@ -40,9 +40,6 @@ import java.util.Set;
 @Component
 public class ArticleTextVectorJob {
 
-    private static final String SOURCE_AIGC = "aigc_deconstruct";
-    private static final String SOURCE_ODPS_TEXT = "odps_text";
-
     /** 每批 DB 写入数量 */
     private static final int DB_BATCH_SIZE = 200;
 
@@ -146,7 +143,7 @@ public class ArticleTextVectorJob {
         while (true) {
             int offset = pageNum * VectorConstants.PAGE_SIZE;
             List<String> page = articleDeconstructResultMapperExt
-                    .selectArticleIdsBySourcePaged(SOURCE_AIGC, offset, VectorConstants.PAGE_SIZE);
+                    .selectArticleIdsBySourcePaged(VectorConstants.SOURCE_AIGC, offset, VectorConstants.PAGE_SIZE);
             if (CollectionUtils.isEmpty(page)) {
                 break;
             }
@@ -259,7 +256,7 @@ public class ArticleTextVectorJob {
 
             ArticleDeconstructResult row = new ArticleDeconstructResult();
             row.setArticleId(articleId);
-            row.setSource(SOURCE_ODPS_TEXT);
+            row.setSource(VectorConstants.SOURCE_ODPS_TEXT);
             row.setResult(json.toJSONString());
             batch.add(row);
 

+ 6 - 8
core/src/main/java/com/tzld/videoVector/job/ArticleVectorJob.java

@@ -51,8 +51,6 @@ import java.util.stream.Collectors;
 @Component
 public class ArticleVectorJob {
 
-    private static final String SOURCE_AIGC = "aigc_deconstruct";
-
     @Resource
     private DeconstructVectorConfigMapper vectorConfigMapper;
 
@@ -121,7 +119,7 @@ public class ArticleVectorJob {
         List<String> allArticleIds = new ArrayList<>(articleIdToTaskInstanceId.keySet());
         for (List<String> batchIds : Lists.partition(allArticleIds, VectorConstants.ODPS_IN_BATCH_SIZE)) {
             Set<String> existingIds = new HashSet<>(
-                    articleDeconstructResultMapperExt.selectExistingArticleIds(SOURCE_AIGC, batchIds));
+                    articleDeconstructResultMapperExt.selectExistingArticleIds(VectorConstants.SOURCE_AIGC, batchIds));
             skipCount.addAndGet(existingIds.size());
 
             List<String> needSyncIds = batchIds.stream()
@@ -144,7 +142,7 @@ public class ArticleVectorJob {
                         if (dataContent != null) {
                             ArticleDeconstructResult r = new ArticleDeconstructResult();
                             r.setArticleId(articleId);
-                            r.setSource(SOURCE_AIGC);
+                            r.setSource(VectorConstants.SOURCE_AIGC);
                             r.setResult(dataContent.toJSONString());
                             batch.add(r);
                         }
@@ -176,9 +174,9 @@ public class ArticleVectorJob {
 
     private ReturnT<String> doVectorize(Integer maxArticleCount) {
         try {
-            List<DeconstructVectorConfig> configs = getEnabledConfigsBySourceField(SOURCE_AIGC);
+            List<DeconstructVectorConfig> configs = getEnabledConfigsBySourceField(VectorConstants.SOURCE_AIGC);
             if (CollectionUtils.isEmpty(configs)) {
-                log.info("未找到 source_field={} 的向量化配置", SOURCE_AIGC);
+                log.info("未找到 source_field={} 的向量化配置", VectorConstants.SOURCE_AIGC);
                 return ReturnT.SUCCESS;
             }
             log.info("加载 {} 个文章向量化配置: {}", configs.size(),
@@ -199,7 +197,7 @@ public class ArticleVectorJob {
                 }
 
                 List<String> articleIds = articleDeconstructResultMapperExt
-                        .selectArticleIdsBySourcePaged(SOURCE_AIGC, offset, limit);
+                        .selectArticleIdsBySourcePaged(VectorConstants.SOURCE_AIGC, offset, limit);
                 if (CollectionUtils.isEmpty(articleIds)) {
                     log.info("第 {} 页没有查询到数据,分页查询结束", pageNum);
                     break;
@@ -242,7 +240,7 @@ public class ArticleVectorJob {
 
     private Map<String, ParsedArticle> loadParsedArticles(List<String> articleIds) {
         List<ArticleDeconstructResult> results = articleDeconstructResultMapperExt
-                .selectResultsByArticleIds(SOURCE_AIGC, articleIds);
+                .selectResultsByArticleIds(VectorConstants.SOURCE_AIGC, articleIds);
         Map<String, ParsedArticle> map = new HashMap<>(articleIds.size());
         for (ArticleDeconstructResult r : results) {
             if (r == null || !StringUtils.hasText(r.getResult())) continue;

+ 95 - 60
core/src/main/java/com/tzld/videoVector/job/ChannelDemandMatchJob.java

@@ -7,6 +7,9 @@ import com.ctrip.framework.apollo.spring.annotation.ApolloJsonValue;
 import com.google.common.collect.Lists;
 import com.tzld.videoVector.api.LibraryApiService;
 import com.tzld.videoVector.common.constant.VectorConstants;
+import com.tzld.videoVector.common.enums.DimensionEnum;
+import com.tzld.videoVector.common.enums.MatchMethodEnum;
+import com.tzld.videoVector.common.enums.PointTypeEnum;
 import com.tzld.videoVector.dao.mapper.pgVector.ChannelDemandMatchConfigMapper;
 import com.tzld.videoVector.dao.mapper.pgVector.ChannelDemandMatchResultMapper;
 import com.tzld.videoVector.dao.mapper.pgVector.ext.ChannelDemandMatchResultMapperExt;
@@ -68,47 +71,12 @@ public class ChannelDemandMatchJob {
     @Resource
     private LibraryApiService libraryApiService;
 
-    /**
-     * 召回结果Redis缓存前缀
-     */
-    private static final String RECALL_CACHE_PREFIX = "channel_demand:recall:";
-
-    /**
-     * Library API 召回结果Redis缓存前缀
-     */
-    private static final String LIBRARY_RECALL_CACHE_PREFIX = "channel_demand:library_recall:";
-
-    /**
-     * 召回结果缓存过期时间(秒)
-     */
-    private static final long RECALL_CACHE_EXPIRE = 6 * 60 * 60;
-
     /**
      * 需求匹配并发线程数(单个维度批次内的需求匹配)
      */
     @Value("${channel.demand.match.thread-pool-size:50}")
     private int matchThreadPoolSize;
 
-    /**
-     * 匹配手段:场景已看视频(来自维度统计表,不走向量召回)
-     */
-    private static final String MATCH_METHOD_DIMENSION_STAT = "场景已看视频";
-
-    /**
-     * 匹配手段:视频库_解构特征_向量相似匹配(走向量召回)
-     */
-    private static final String MATCH_METHOD_RECOMMEND_LIB = "视频库_解构特征_向量相似匹配";
-
-    /**
-     * 匹配手段:视频库_解构特征点_精准匹配(Library API 精准匹配)
-     */
-    private static final String MATCH_METHOD_RECOMMEND_LIB_EXACT = "视频库_解构特征点_精准匹配";
-
-    /**
-     * 需求来源:优质相似(对应 demand_strategy 取值)
-     */
-    private static final String DEMAND_SOURCE_QUALITY_SIM = "优质相似";
-
     // ============== 匹配结果默认值(固定文案) ==============
 
     /**
@@ -208,17 +176,6 @@ public class ChannelDemandMatchJob {
     @Value("${video.detail.metrics.days:7}")
     private int metricsDays;
 
-    /**
-     * 点类型 → 向量配置编码映射
-     */
-    private static final Map<String, List<String>> POINT_TYPE_CONFIG_CODE_MAP = new HashMap<>();
-
-    static {
-        POINT_TYPE_CONFIG_CODE_MAP.put("关键点", Arrays.asList("VIDEO_KEYPOINT", "KEYPOINT_SUBSTANCE", "KEYPOINT_FORM"));
-        POINT_TYPE_CONFIG_CODE_MAP.put("灵感点", Arrays.asList("VIDEO_INSPIRATION", "INSPIRATION_SUBSTANCE", "INSPIRATION_FORM"));
-        POINT_TYPE_CONFIG_CODE_MAP.put("目的点", Arrays.asList("VIDEO_PURPOSE", "PURPOSE_SUBSTANCE", "PURPOSE_FORM"));
-    }
-
     @PostConstruct
     public void initConfigExecutor() {
         this.configExecutor = new ThreadPoolExecutor(
@@ -354,7 +311,7 @@ public class ChannelDemandMatchJob {
         deleteExistingResults(config.getId(), dt);
 
         // 如果匹配手段为"场景已看视频",走独立的处理逻辑(不需要向量召回)
-        if (MATCH_METHOD_DIMENSION_STAT.equals(config.getMatchMethod())) {
+        if (MatchMethodEnum.DIMENSION_STAT.getValue().equals(config.getMatchMethod())) {
             processDimensionStatSource(config, dt, totalDemands, totalMatched, totalFailed);
             return;
         }
@@ -474,6 +431,63 @@ public class ChannelDemandMatchJob {
         return result;
     }
 
+    /**
+     * 对"传播的分发"/"增长的分发"维度按渠道类分组取TopN%
+     * 与filterTopRovByGroup的区别:分组键为渠道类+维度(不含crowdSegment和channelLevel3)
+     *
+     * @param demands     待过滤的需求列表
+     * @param channelName 渠道名称,用于匹配Apollo配置
+     */
+    private List<ChannelDemandMatchResult> filterTopRovByChannelForDistDimension(
+            List<ChannelDemandMatchResult> demands, String channelName) {
+        if (CollectionUtils.isEmpty(demands)) {
+            return Collections.emptyList();
+        }
+        // 获取该渠道的维度→比例配置
+        Map<String, Double> channelFilterConfig = topRovFilterConfig != null
+                ? topRovFilterConfig.getOrDefault(channelName, Collections.emptyMap())
+                : Collections.emptyMap();
+
+        if (channelFilterConfig.isEmpty()) {
+            return new ArrayList<>(demands);
+        }
+
+        List<ChannelDemandMatchResult> toFilter = new ArrayList<>();
+        List<ChannelDemandMatchResult> keepAsIs = new ArrayList<>();
+        for (ChannelDemandMatchResult d : demands) {
+            if (channelFilterConfig.containsKey(d.getDimension())) {
+                toFilter.add(d);
+            } else {
+                keepAsIs.add(d);
+            }
+        }
+
+        List<ChannelDemandMatchResult> result = new ArrayList<>(keepAsIs);
+
+        if (toFilter.isEmpty()) {
+            return result;
+        }
+
+        // 按渠道类+维度分组(渠道类用于兼容多渠道场景,维度用于区分不同配置比例)
+        Map<String, List<ChannelDemandMatchResult>> grouped = toFilter.stream()
+                .collect(Collectors.groupingBy(d ->
+                        nullToEmpty(d.getChannelName()) + "|"
+                                + nullToEmpty(d.getDimension())));
+        for (List<ChannelDemandMatchResult> group : grouped.values()) {
+            group.sort((a, b) -> {
+                double rovA = a.getTotalRov() != null ? a.getTotalRov() : 0.0;
+                double rovB = b.getTotalRov() != null ? b.getTotalRov() : 0.0;
+                return Double.compare(rovB, rovA);
+            });
+            // 从分组中取任一元素的dimension获取配置比例
+            String dim = group.get(0).getDimension();
+            double ratio = channelFilterConfig.getOrDefault(dim, 0.25);
+            int topCount = Math.max(5, (int) Math.ceil(group.size() * ratio));
+            result.addAll(group.subList(0, Math.min(topCount, group.size())));
+        }
+        return result;
+    }
+
     /**
      * 解析ODPS单条需求记录
      */
@@ -521,8 +535,8 @@ public class ChannelDemandMatchJob {
         result.setMatchStatus((short) 0); // 待匹配
 
         // 向量召回路径:匹配手段固定为视频库_解构特征_向量相似匹配;筛选/排序默认值按需求来源选择
-        result.setMatchMethod(MATCH_METHOD_RECOMMEND_LIB);
-        if (DEMAND_SOURCE_QUALITY_SIM.equals(result.getDemandStrategy())) {
+        result.setMatchMethod(MatchMethodEnum.RECOMMEND_LIB.getValue());
+        if ("优质相似".equals(result.getDemandStrategy())) {
             result.setMatchVideoFilter(VIDEO_FILTER_QUALITY_SIM_RECOMMEND);
             result.setMatchSort(SORT_QUALITY_SIM_RECOMMEND);
         } else {
@@ -557,7 +571,7 @@ public class ChannelDemandMatchJob {
 
         // 策略三:需求特征点类型+需求特征点 均有值 → 用需求特征点召回
         if ("特征点".equals(demand.getDemandType()) && hasValidValue(demand.getPointType()) && hasValidValue(demand.getStandardElement())) {
-            List<String> configCodes = POINT_TYPE_CONFIG_CODE_MAP.getOrDefault(demand.getPointType(), Arrays.asList("VIDEO_TOPIC"));
+            List<String> configCodes = PointTypeEnum.getConfigCodesByValue(demand.getPointType());
             List<ChannelDemandMatchResult> rows = new ArrayList<>();
             for (String configCode : configCodes) {
                 rows.addAll(doRecall(demand, demand.getStandardElement(), configCode, topN / configCodes.size()));
@@ -572,7 +586,7 @@ public class ChannelDemandMatchJob {
             List<ChannelDemandMatchResult> rows = doLibraryRecall(demand, topN);
             allBatchRows.addAll(rows);
             // 向量匹配
-            List<String> configCodes = POINT_TYPE_CONFIG_CODE_MAP.getOrDefault(demand.getMatchGeneralizedPointType(), Arrays.asList("VIDEO_TOPIC"));
+            List<String> configCodes = PointTypeEnum.getConfigCodesByValue(demand.getMatchGeneralizedPointType());
             for (String configCode : configCodes) {
                 allBatchRows.addAll(doRecall(demand, demand.getMatchGeneralizedElement(), configCode, topN / configCodes.size()));
             }
@@ -651,7 +665,7 @@ public class ChannelDemandMatchJob {
      */
     private List<ChannelDemandMatchResult> doLibraryRecall(ChannelDemandMatchResult demand, int topN) {
         String elementName = demand.getMatchGeneralizedElement();
-        String cacheKey = LIBRARY_RECALL_CACHE_PREFIX + Md5Util.encoderByMd5(elementName);
+        String cacheKey = VectorConstants.CHANNEL_DEMAND_LIBRARY_RECALL_CACHE_PREFIX + Md5Util.encoderByMd5(elementName);
 
         // 1. 尝试从缓存读取
         List<CachedPost> cachedPosts = loadLibraryRecallCache(cacheKey);
@@ -750,7 +764,7 @@ public class ChannelDemandMatchJob {
 
         // 7. 写入缓存
         try {
-            redisUtils.set(cacheKey, JSON.toJSONString(newCachedPosts), RECALL_CACHE_EXPIRE);
+            redisUtils.set(cacheKey, JSON.toJSONString(newCachedPosts), VectorConstants.CHANNEL_DEMAND_RECALL_CACHE_EXPIRE);
         } catch (Exception e) {
             log.warn("写入Library API召回缓存失败, key={}: {}", cacheKey, e.getMessage());
         }
@@ -790,7 +804,7 @@ public class ChannelDemandMatchJob {
             Long postId = Long.parseLong(cp.getPostId());
 
             ChannelDemandMatchResult row = copyDemandFields(demand);
-            row.setMatchMethod(MATCH_METHOD_RECOMMEND_LIB_EXACT);
+            row.setMatchMethod(MatchMethodEnum.RECOMMEND_LIB_EXACT.getValue());
             if (StringUtils.hasText(row.getDemandType())) {
                 if ("特征点".equals(row.getDemandType())) {
                     row.setDemandType("聚类特征点");
@@ -848,7 +862,7 @@ public class ChannelDemandMatchJob {
      * 带Redis缓存的召回:相同queryText+configCode+topN直接复用缓存结果
      */
     private RecallVideoScoreVO getRecallResultWithCache(RecallVideoScoreParam param) {
-        String cacheKey = RECALL_CACHE_PREFIX + Md5Util.encoderByMd5(
+        String cacheKey = VectorConstants.CHANNEL_DEMAND_RECALL_CACHE_PREFIX + Md5Util.encoderByMd5(
                 param.getQueryText() + "|" + param.getConfigCode() + "|" + param.getTopN());
         try {
             String cached = redisUtils.get(cacheKey);
@@ -863,7 +877,7 @@ public class ChannelDemandMatchJob {
 
         if (scoreVO != null && !CollectionUtils.isEmpty(scoreVO.getItems())) {
             try {
-                redisUtils.set(cacheKey, JSON.toJSONString(scoreVO), RECALL_CACHE_EXPIRE);
+                redisUtils.set(cacheKey, JSON.toJSONString(scoreVO), VectorConstants.CHANNEL_DEMAND_RECALL_CACHE_EXPIRE);
             } catch (Exception e) {
                 log.warn("写入召回缓存失败, key={}: {}", cacheKey, e.getMessage());
             }
@@ -1011,7 +1025,7 @@ public class ChannelDemandMatchJob {
                 // 数据源差异由 config.match_method 区分;此处 demand_strategy 与配置保持一致(迁移后统一为"人群需求")
                 result.setDemandStrategy(config.getDemandStrategy());
                 // 匹配手段/筛选/排序 固定默认值(场景已看视频)
-                result.setMatchMethod(MATCH_METHOD_DIMENSION_STAT);
+                result.setMatchMethod(MatchMethodEnum.DIMENSION_STAT.getValue());
                 result.setMatchVideoFilter(VIDEO_FILTER_DIMENSION_STAT);
                 result.setMatchSort(SORT_DIMENSION_STAT);
 
@@ -1095,8 +1109,29 @@ public class ChannelDemandMatchJob {
         // 按Apollo配置对增长的头部等维度执行分组TOP过滤
         String channelName = config.getChannelName();
         int beforeFilterCount = results.size();
-        List<ChannelDemandMatchResult> filteredResults = filterTopRovByGroup(results, channelName);
-        log.info("场景已看视频 渠道 {} 分组过滤后 {} -> {} 条", channelName, beforeFilterCount, filteredResults.size());
+
+        // 分离头部维度和分发维度:分发维度按渠道类分组过滤,头部维度保持原有逻辑
+        List<ChannelDemandMatchResult> headResults = new ArrayList<>();
+        List<ChannelDemandMatchResult> distResults = new ArrayList<>();
+        for (ChannelDemandMatchResult r : results) {
+            if (DimensionEnum.distributionValueSet().contains(r.getDimension())) {
+                distResults.add(r);
+            } else {
+                headResults.add(r);
+            }
+        }
+
+        // 头部维度:保持现有逻辑(按 crowdSegment|channelLevel3|dimension 分组过滤)
+        List<ChannelDemandMatchResult> filteredHead = filterTopRovByGroup(headResults, channelName);
+        // 分发维度:按 渠道类 分组取前百分之几
+        List<ChannelDemandMatchResult> filteredDist = filterTopRovByChannelForDistDimension(distResults, channelName);
+
+        List<ChannelDemandMatchResult> filteredResults = new ArrayList<>();
+        filteredResults.addAll(filteredHead);
+        filteredResults.addAll(filteredDist);
+
+        log.info("场景已看视频 渠道 {} 分组过滤后 {} -> {} 条 (头部{}条, 分发{}条)",
+                channelName, beforeFilterCount, filteredResults.size(), filteredHead.size(), filteredDist.size());
 
         // 批量写入
         for (List<ChannelDemandMatchResult> partition : Lists.partition(filteredResults, 1000)) {
@@ -1151,7 +1186,7 @@ public class ChannelDemandMatchJob {
         sb.append(" AND 总uv占比 > ").append(dimensionStatMinUvRatio);
         sb.append(" AND 总访问uv > 2000 AND 全局分发pv > 10000 ");
 //        sb.append(" AND `merge二级品类` not in ('早中晚好','节日祝福') ");
-        sb.append(" AND 维度 in ('传播的头部', '增长的头部') ");
+        sb.append(" AND 维度 in (").append(DimensionEnum.toSqlInValues()).append(") ");
         // 渠道筛选
         if (StringUtils.hasText(config.getChannelName())) {
             sb.append(" AND 渠道类 = '").append(config.getChannelName().replace("'", "''")).append("'");

+ 5 - 3
core/src/main/java/com/tzld/videoVector/job/MaterialDeconstructCheckJob.java

@@ -4,6 +4,7 @@ import com.tzld.videoVector.dao.mapper.pgVector.DeconstructContentMapper;
 import com.tzld.videoVector.model.entity.DeconstructResult;
 import com.tzld.videoVector.model.po.pgVector.DeconstructContent;
 import com.tzld.videoVector.model.po.pgVector.DeconstructContentExample;
+import com.tzld.videoVector.common.enums.DeconstructStatusEnum;
 import com.tzld.videoVector.service.DeconstructService;
 import com.tzld.videoVector.service.VectorizeService;
 import com.xxl.job.core.biz.model.ReturnT;
@@ -106,7 +107,8 @@ public class MaterialDeconstructCheckJob {
         DeconstructContentExample example = new DeconstructContentExample();
         DeconstructContentExample.Criteria criteria = example.createCriteria();
         // status IN (0, 1)
-        List<Short> pendingStatuses = java.util.Arrays.asList((short) 0, (short) 1);
+        List<Short> pendingStatuses = java.util.Arrays.asList(
+                DeconstructStatusEnum.PENDING.getCode(), DeconstructStatusEnum.RUNNING.getCode());
         criteria.andStatusIn(pendingStatuses);
         example.setOrderByClause("id ASC LIMIT " + pageSize + " OFFSET " + (pageNum * pageSize));
         return deconstructContentMapper.selectByExample(example);
@@ -151,9 +153,9 @@ public class MaterialDeconstructCheckJob {
             log.info("更新解构状态,contentId={}, taskId={}, status={} -> {}",
                     content.getId(), taskId, content.getStatus(), newStatus);
 
-            if (newStatus == 2) {
+            if (DeconstructStatusEnum.SUCCESS.getCode() == newStatus) {
                 return 1;
-            } else if (newStatus == 3) {
+            } else if (DeconstructStatusEnum.FAILED.getCode() == newStatus) {
                 log.error("解构失败,contentId={}, taskId={}, reason={}",
                         content.getId(), taskId, result.getReason());
                 return -1;

+ 6 - 8
core/src/main/java/com/tzld/videoVector/job/MaterialVectorJob.java

@@ -63,8 +63,6 @@ import java.util.stream.Collectors;
 @Component
 public class MaterialVectorJob {
 
-    private static final String SOURCE_AIGC = "aigc_deconstruct";
-
     @Resource
     private DeconstructVectorConfigMapper vectorConfigMapper;
 
@@ -185,7 +183,7 @@ public class MaterialVectorJob {
         List<String> allMaterialIds = new ArrayList<>(materialIdToTaskInstanceId.keySet());
         for (List<String> batchIds : Lists.partition(allMaterialIds, VectorConstants.ODPS_IN_BATCH_SIZE)) {
             Set<String> existingIds = new HashSet<>(
-                    materialDeconstructResultMapperExt.selectExistingMaterialIds(SOURCE_AIGC, batchIds));
+                    materialDeconstructResultMapperExt.selectExistingMaterialIds(VectorConstants.SOURCE_AIGC, batchIds));
             skipCount.addAndGet(existingIds.size());
 
             List<String> needSyncIds = batchIds.stream()
@@ -209,7 +207,7 @@ public class MaterialVectorJob {
                         if (dataContent != null) {
                             MaterialDeconstructResult r = new MaterialDeconstructResult();
                             r.setMaterialId(materialId);
-                            r.setSource(SOURCE_AIGC);
+                            r.setSource(VectorConstants.SOURCE_AIGC);
                             r.setResult(dataContent.toJSONString());
                             r.setSourceType(materialIdToSourceType.getOrDefault(materialId, defaultSourceType));
                             batch.add(r);
@@ -253,9 +251,9 @@ public class MaterialVectorJob {
     private ReturnT<String> doVectorize(Integer maxMaterialCount) {
         try {
             // 1. 加载素材专用向量配置
-            List<DeconstructVectorConfig> configs = getEnabledConfigsBySourceField(SOURCE_AIGC);
+            List<DeconstructVectorConfig> configs = getEnabledConfigsBySourceField(VectorConstants.SOURCE_AIGC);
             if (CollectionUtils.isEmpty(configs)) {
-                log.info("未找到 source_field={} 的向量化配置", SOURCE_AIGC);
+                log.info("未找到 source_field={} 的向量化配置", VectorConstants.SOURCE_AIGC);
                 return ReturnT.SUCCESS;
             }
             log.info("加载 {} 个素材向量化配置: {}", configs.size(),
@@ -276,7 +274,7 @@ public class MaterialVectorJob {
                 }
 
                 List<String> materialIds = materialDeconstructResultMapperExt
-                        .selectMaterialIdsBySourcePaged(SOURCE_AIGC, offset, limit);
+                        .selectMaterialIdsBySourcePaged(VectorConstants.SOURCE_AIGC, offset, limit);
                 if (CollectionUtils.isEmpty(materialIds)) {
                     log.info("第 {} 页没有查询到数据,分页查询结束", pageNum);
                     break;
@@ -324,7 +322,7 @@ public class MaterialVectorJob {
      */
     private Map<String, ParsedMaterial> loadParsedMaterials(List<String> materialIds) {
         List<MaterialDeconstructResult> results = materialDeconstructResultMapperExt
-                .selectResultsByMaterialIds(SOURCE_AIGC, materialIds);
+                .selectResultsByMaterialIds(VectorConstants.SOURCE_AIGC, materialIds);
         Map<String, ParsedMaterial> map = new HashMap<>(materialIds.size());
         for (MaterialDeconstructResult r : results) {
             if (r == null || !StringUtils.hasText(r.getResult())) continue;

+ 11 - 9
core/src/main/java/com/tzld/videoVector/job/VideoVectorJob.java

@@ -8,6 +8,7 @@ import com.google.common.collect.Lists;
 import com.tzld.videoVector.api.AigcApiService;
 import com.tzld.videoVector.api.VideoApiService;
 import com.tzld.videoVector.common.constant.VectorConstants;
+import com.tzld.videoVector.common.enums.DeconstructStatusEnum;
 import com.tzld.videoVector.dao.mapper.pgVector.DeconstructContentMapper;
 import com.tzld.videoVector.dao.mapper.pgVector.DeconstructVectorConfigMapper;
 import com.tzld.videoVector.dao.mapper.pgVector.ext.VideoDeconstructResultMapperExt;
@@ -519,7 +520,7 @@ public class VideoVectorJob {
         log.info("开始执行 AIGC 来源视频向量化任务, param: {}", param);
         try {
             // 1. 获取 aigc_deconstruct 专用的向量化配置
-            List<DeconstructVectorConfig> configs = getEnabledConfigsBySourceField("aigc_deconstruct");
+            List<DeconstructVectorConfig> configs = getEnabledConfigsBySourceField(VectorConstants.SOURCE_AIGC);
             if (CollectionUtils.isEmpty(configs)) {
                 log.info("未找到 aigc_deconstruct 来源的向量化配置");
                 return ReturnT.SUCCESS;
@@ -533,7 +534,7 @@ public class VideoVectorJob {
             while (true) {
                 int offset = pageNum * VectorConstants.PAGE_SIZE;
                 List<Long> videoIds = videoDeconstructResultMapperExt.selectVideoIdsBySourcePaged(
-                        "aigc_deconstruct", offset, VectorConstants.PAGE_SIZE);
+                        VectorConstants.SOURCE_AIGC, offset, VectorConstants.PAGE_SIZE);
                 if (CollectionUtils.isEmpty(videoIds)) {
                     log.info("第 {} 页没有查询到数据,分页查询结束", pageNum);
                     break;
@@ -600,7 +601,7 @@ public class VideoVectorJob {
 
             // 3. 从本地DB批量查询解构结果并顺序embedding
             List<VideoDeconstructResult> results = videoDeconstructResultMapperExt
-                    .selectResultsByVideoIds("aigc_deconstruct", needProcessIds);
+                    .selectResultsByVideoIds(VectorConstants.SOURCE_AIGC, needProcessIds);
             for (VideoDeconstructResult r : results) {
                 if (!StringUtils.hasText(r.getResult())) {
                     continue;
@@ -887,7 +888,8 @@ public class VideoVectorJob {
             Date timeoutThreshold = new Date(System.currentTimeMillis() - VectorConstants.TIMEOUT_MS);
 
             DeconstructContentExample example = new DeconstructContentExample();
-            example.createCriteria().andStatusIn(Arrays.asList((short) 0, (short) 1))  // PENDING=0, RUNNING=1
+            example.createCriteria().andStatusIn(Arrays.asList(
+                    DeconstructStatusEnum.PENDING.getCode(), DeconstructStatusEnum.RUNNING.getCode()))
                     .andCreateTimeLessThanOrEqualTo(timeoutThreshold);
             List<DeconstructContent> timeoutTasks = deconstructContentMapper.selectByExample(example);
 
@@ -915,7 +917,7 @@ public class VideoVectorJob {
 
                         if (result == null) {
                             log.error("重试解构任务失败,API返回空,taskId={}", taskId);
-                            updateContentStatus(content, (byte) 3, "API返回空");
+                            updateContentStatus(content, DeconstructStatusEnum.FAILED.getCode(), "API返回空");
                             failCount.incrementAndGet();
                             return;
                         }
@@ -924,7 +926,7 @@ public class VideoVectorJob {
                         if (result.isFinished()) {
                             if (result.isSuccess()) {
                                 // 成功
-                                content.setStatus((short) 2);
+                                content.setStatus(DeconstructStatusEnum.SUCCESS.getCode());
                                 content.setResultJson(result.getResult());
                                 content.setPointUrl(result.getPointUrl());
                                 content.setWeightUrl(result.getWeightUrl());
@@ -935,7 +937,7 @@ public class VideoVectorJob {
                                 log.info("重试解构任务成功,taskId={}", taskId);
                             } else {
                                 // 失败
-                                updateContentStatus(content, (short) 3, result.getReason());
+                                updateContentStatus(content, DeconstructStatusEnum.FAILED.getCode(), result.getReason());
                                 failCount.incrementAndGet();
                                 log.error("重试解构任务失败,taskId={}, reason={}", taskId, result.getReason());
                             }
@@ -1422,7 +1424,7 @@ public class VideoVectorJob {
         List<Long> allVideoIds = new ArrayList<>(videoIdToTaskInstanceId.keySet());
         // 分批检查已存在的
         for (List<Long> batchIds : Lists.partition(allVideoIds, VectorConstants.ODPS_IN_BATCH_SIZE)) {
-            Set<Long> existingIds = new HashSet<>(videoDeconstructResultMapperExt.selectExistingVideoIds("aigc_deconstruct", batchIds));
+            Set<Long> existingIds = new HashSet<>(videoDeconstructResultMapperExt.selectExistingVideoIds(VectorConstants.SOURCE_AIGC, batchIds));
             skipCount.addAndGet(existingIds.size());
 
             List<Long> needSyncIds = batchIds.stream()
@@ -1443,7 +1445,7 @@ public class VideoVectorJob {
                         if (dataContent != null) {
                             VideoDeconstructResult r = new VideoDeconstructResult();
                             r.setVideoId(videoId);
-                            r.setSource("aigc_deconstruct");
+                            r.setSource(VectorConstants.SOURCE_AIGC);
                             r.setResult(dataContent.toJSONString());
                             batch.add(r);
                         }

+ 3 - 1
core/src/main/java/com/tzld/videoVector/service/impl/DeconstructServiceImpl.java

@@ -1,6 +1,7 @@
 package com.tzld.videoVector.service.impl;
 
 import com.alibaba.fastjson.JSONObject;
+import com.tzld.videoVector.common.enums.DeconstructStatusEnum;
 import com.tzld.videoVector.model.entity.DeconstructResult;
 import com.tzld.videoVector.service.DeconstructService;
 import lombok.extern.slf4j.Slf4j;
@@ -184,7 +185,8 @@ public class DeconstructServiceImpl implements DeconstructService {
                 result.setStatus(data.getInteger("status"));
                 result.setResult(data.getString("result"));
                 result.setReason(data.getString("reason"));
-                result.setSuccess(result.getStatus() != null && result.getStatus() == 2);
+                result.setSuccess(result.getStatus() != null
+                        && DeconstructStatusEnum.SUCCESS.getCode() == result.getStatus());
 
                 // 解析 url 对象
                 JSONObject urlObj = data.getJSONObject("url");

+ 17 - 12
core/src/main/java/com/tzld/videoVector/service/impl/MaterialSearchServiceImpl.java

@@ -1,6 +1,10 @@
 package com.tzld.videoVector.service.impl;
 
 import com.alibaba.fastjson.JSONObject;
+import com.tzld.videoVector.common.constant.VectorConstants;
+import com.tzld.videoVector.common.enums.DeconstructStatusEnum;
+import com.tzld.videoVector.common.enums.PointTypeEnum;
+import com.tzld.videoVector.common.enums.SourceTypeEnum;
 import com.tzld.videoVector.dao.mapper.pgVector.DeconstructContentMapper;
 import com.tzld.videoVector.dao.mapper.pgVector.DeconstructVectorConfigMapper;
 import com.tzld.videoVector.dao.mapper.pgVector.ext.ContentVectorMapperExt;
@@ -110,7 +114,7 @@ public class MaterialSearchServiceImpl implements MaterialSearchService {
             DeconstructContent existing = getDeconstructContentByChannelContentId(channelContentId);
             if (existing != null) {
                 Short status = existing.getStatus();
-                if (status != null && status != 3) {
+                if (status != null && DeconstructStatusEnum.isNotFailed(status)) {
                     log.info("素材已存在,channelContentId={}, taskId={}, status={}, 不重复提交",
                             channelContentId, existing.getTaskId(), status);
                     return existing.getTaskId();
@@ -146,14 +150,14 @@ public class MaterialSearchServiceImpl implements MaterialSearchService {
         try {
             if (failedContent != null) {
                 failedContent.setTaskId(taskId);
-                failedContent.setBizType((short) 0);
+                failedContent.setBizType(DeconstructStatusEnum.PENDING.getCode());
                 failedContent.setContentType(contentType.shortValue());
                 failedContent.setTitle(param.getTitle());
                 failedContent.setBodyText(param.getBodyText());
                 failedContent.setVideoUrl(param.getVideoUrl());
                 failedContent.setChannelAccountId(param.getChannelAccountId());
                 failedContent.setChannelAccountName(param.getChannelAccountName());
-                failedContent.setStatus((short) 0);
+                failedContent.setStatus(DeconstructStatusEnum.PENDING.getCode());
                 failedContent.setResultJson("");
                 failedContent.setFailureReason("");
                 failedContent.setPointUrl("");
@@ -168,7 +172,7 @@ public class MaterialSearchServiceImpl implements MaterialSearchService {
             } else {
                 DeconstructContent content = new DeconstructContent();
                 content.setTaskId(taskId);
-                content.setBizType((short) 0);
+                content.setBizType(DeconstructStatusEnum.PENDING.getCode());
                 content.setContentType(contentType.shortValue());
                 content.setChannelContentId(param.getChannelContentId());
                 content.setTitle(param.getTitle());
@@ -176,7 +180,7 @@ public class MaterialSearchServiceImpl implements MaterialSearchService {
                 content.setVideoUrl(param.getVideoUrl());
                 content.setChannelAccountId(param.getChannelAccountId());
                 content.setChannelAccountName(param.getChannelAccountName());
-                content.setStatus((short) 0);
+                content.setStatus(DeconstructStatusEnum.PENDING.getCode());
                 content.setCreateTime(new Date());
                 content.setUpdateTime(new Date());
                 if (param.getImageList() != null && !param.getImageList().isEmpty()) {
@@ -616,7 +620,7 @@ public class MaterialSearchServiceImpl implements MaterialSearchService {
         }
         try {
             List<MaterialDeconstructResult> rows = materialDeconstructResultMapperExt
-                    .selectResultsByMaterialIds("aigc_deconstruct", materialIds);
+                    .selectResultsByMaterialIds(VectorConstants.SOURCE_AIGC, materialIds);
             if (rows == null || rows.isEmpty()) {
                 return Collections.emptyMap();
             }
@@ -688,9 +692,9 @@ public class MaterialSearchServiceImpl implements MaterialSearchService {
             result.put("topic", topic);
         }
         // 灵感点/关键点/目的点 — 提取名称列表
-        extractPointNames(json, result, "灵感点");
-        extractPointNames(json, result, "关键点");
-        extractPointNames(json, result, "目的点");
+        extractPointNames(json, result, PointTypeEnum.INSPIRATION.getValue());
+        extractPointNames(json, result, PointTypeEnum.KEY_POINT.getValue());
+        extractPointNames(json, result, PointTypeEnum.PURPOSE.getValue());
         return result.isEmpty() ? null : result;
     }
 
@@ -757,8 +761,8 @@ public class MaterialSearchServiceImpl implements MaterialSearchService {
 
     private static String mapSourceTypeLabel(Short sourceType) {
         if (sourceType == null) return null;
-        if (sourceType == 1) return "外部合作";
-        if (sourceType == 2) return "内部素材";
+        if (sourceType == SourceTypeEnum.EXTERNAL.getCode()) return SourceTypeEnum.EXTERNAL.getLabel();
+        if (sourceType == SourceTypeEnum.INTERNAL.getCode()) return SourceTypeEnum.INTERNAL.getLabel();
         return null;
     }
 
@@ -876,7 +880,8 @@ public class MaterialSearchServiceImpl implements MaterialSearchService {
     private void triggerVectorizeIfNeeded(String channelContentId, String configCode) {
         try {
             DeconstructContent content = getDeconstructContentByChannelContentId(channelContentId);
-            if (content == null || content.getStatus() == null || content.getStatus() != 2) {
+            if (content == null || content.getStatus() == null
+                    || !DeconstructStatusEnum.matchCode(content.getStatus(), DeconstructStatusEnum.SUCCESS)) {
                 return;
             }
             List<ContentVector> vectors =

+ 17 - 17
core/src/main/java/com/tzld/videoVector/service/impl/VideoSearchServiceImpl.java

@@ -4,6 +4,8 @@ import com.alibaba.fastjson.JSONArray;
 import com.alibaba.fastjson.JSONObject;
 import com.tzld.videoVector.api.VideoApiService;
 import com.tzld.videoVector.common.constant.VectorConstants;
+import com.tzld.videoVector.common.enums.DeconstructStatusEnum;
+import com.tzld.videoVector.common.enums.PointTypeEnum;
 import com.tzld.videoVector.dao.mapper.pgVector.ChannelDemandMatchResultMapper;
 import com.tzld.videoVector.dao.mapper.pgVector.DeconstructContentMapper;
 import com.tzld.videoVector.dao.mapper.pgVector.DeconstructVectorConfigMapper;
@@ -104,7 +106,7 @@ public class VideoSearchServiceImpl implements VideoSearchService {
                 Short status = existingContent.getStatus();
                 // 状态: 0-PENDING, 1-RUNNING, 2-SUCCESS, 3-FAILED
                 // 如果状态为 PENDING、RUNNING 或 SUCCESS,不重复提交,直接返回已有 taskId
-                if (status != null && status != 3) {
+                if (status != null && DeconstructStatusEnum.isNotFailed(status)) {
                     log.info("解构任务已存在,channelContentId={}, taskId={}, status={}, 不重复提交",
                             channelContentId, existingContent.getTaskId(), getStatusDesc(status));
                     return existingContent.getTaskId();
@@ -152,7 +154,7 @@ public class VideoSearchServiceImpl implements VideoSearchService {
                 failedContent.setVideoUrl(param.getVideoUrl());
                 failedContent.setChannelAccountId(param.getChannelAccountId());
                 failedContent.setChannelAccountName(param.getChannelAccountName());
-                failedContent.setStatus((short) 0); // PENDING
+                failedContent.setStatus(DeconstructStatusEnum.PENDING.getCode());
                 // 清除历史失败信息(使用空字符串确保 selective update 生效)
                 failedContent.setResultJson("");
                 failedContent.setFailureReason("");
@@ -177,7 +179,7 @@ public class VideoSearchServiceImpl implements VideoSearchService {
                 content.setVideoUrl(param.getVideoUrl());
                 content.setChannelAccountId(param.getChannelAccountId());
                 content.setChannelAccountName(param.getChannelAccountName());
-                content.setStatus((short) 0); // PENDING
+                content.setStatus(DeconstructStatusEnum.PENDING.getCode());
                 content.setCreateTime(new Date());
                 content.setUpdateTime(new Date());
                 // 处理图片列表
@@ -211,10 +213,9 @@ public class VideoSearchServiceImpl implements VideoSearchService {
         // 1. 数据库中不存在记录 -> 调用API
         // 2. 数据库中记录状态非终态(PENDING/RUNNING) -> 调用API刷新
         // 3. 强制刷新参数 -> 调用API
-        boolean needRefresh = content == null 
-                || content.getStatus() == null 
-                || content.getStatus() == 0 
-                || content.getStatus() == 1
+        boolean needRefresh = content == null
+                || content.getStatus() == null
+                || !DeconstructStatusEnum.isTerminalCode(content.getStatus())
                 || Boolean.TRUE.equals(param.getForceRefresh());
 
         DeconstructResult result = null;
@@ -486,7 +487,7 @@ public class VideoSearchServiceImpl implements VideoSearchService {
             }
 
             // 更新状态和结果
-            content.setStatus(result.getStatus() != null ? result.getStatus().shortValue() : (short) 3);
+            content.setStatus(result.getStatus() != null ? result.getStatus().shortValue() : DeconstructStatusEnum.FAILED.getCode());
             content.setResultJson(result.getResult());
             content.setFailureReason(result.getReason());
             content.setPointUrl(result.getPointUrl());
@@ -517,8 +518,8 @@ public class VideoSearchServiceImpl implements VideoSearchService {
         jsonResult.put("taskId", content.getTaskId());
         jsonResult.put("status", content.getStatus());
         jsonResult.put("statusDesc", getStatusDesc(content.getStatus()));
-        jsonResult.put("success", content.getStatus() != null && content.getStatus() == 2);
-        jsonResult.put("finished", content.getStatus() != null && (content.getStatus() == 2 || content.getStatus() == 3));
+        jsonResult.put("success", DeconstructStatusEnum.matchCode(content.getStatus(), DeconstructStatusEnum.SUCCESS));
+        jsonResult.put("finished", DeconstructStatusEnum.isTerminalCode(content.getStatus()));
         jsonResult.put("fromCache", true);
 
         if (content.getResultJson() != null) {
@@ -1044,8 +1045,7 @@ public class VideoSearchServiceImpl implements VideoSearchService {
 
         // 如果状态非终态,尝试从API刷新
         boolean needRefresh = content.getStatus() == null
-                || content.getStatus() == 0
-                || content.getStatus() == 1;
+                || !DeconstructStatusEnum.isTerminalCode(content.getStatus());
 
         if (needRefresh && StringUtils.hasText(content.getTaskId())) {
             log.info("解构任务未完成,调用API刷新,channelContentId={}, taskId={}, status={}",
@@ -1200,7 +1200,7 @@ public class VideoSearchServiceImpl implements VideoSearchService {
         // 按 type 分桶: name 列表 + essences flatten
         Map<String, List<String>> nameByType = new LinkedHashMap<>();
         Map<String, List<Map<String, Object>>> essenceByType = new LinkedHashMap<>();
-        for (String t : new String[]{"灵感点", "关键点", "目的点"}) {
+        for (String t : PointTypeEnum.getAllValueList()) {
             nameByType.put(t, new ArrayList<>());
             essenceByType.put(t, new ArrayList<>());
         }
@@ -1236,7 +1236,7 @@ public class VideoSearchServiceImpl implements VideoSearchService {
             }
         }
 
-        for (String t : new String[]{"灵感点", "关键点", "目的点"}) {
+        for (String t : PointTypeEnum.getAllValueList()) {
             out.put(t, nameByType.get(t));
             out.put(t + "-实质", essenceByType.get(t));
         }
@@ -1303,7 +1303,7 @@ public class VideoSearchServiceImpl implements VideoSearchService {
         // ④ 灵感点/关键点/目的点: point_label 优先,point_classification 备选
         JSONObject pl = raw.getJSONObject("point_label");
         JSONObject pcl = raw.getJSONObject("point_classification");
-        for (String pointType : new String[]{"灵感点", "关键点", "目的点"}) {
+        for (String pointType : PointTypeEnum.getAllValueList()) {
             JSONArray arr = (pl != null) ? pl.getJSONArray(pointType) : null;
             if (arr == null && pcl != null) {
                 arr = pcl.getJSONArray(pointType);
@@ -1334,7 +1334,7 @@ public class VideoSearchServiceImpl implements VideoSearchService {
         flat.put("purpose_final_result", raw.getJSONObject("purpose_final_result"));
 
         // ③ 灵感点/关键点/目的点: 顶层数组
-        for (String pointType : new String[]{"灵感点", "关键点", "目的点"}) {
+        for (String pointType : PointTypeEnum.getAllValueList()) {
             JSONArray arr = raw.getJSONArray(pointType);
             if (arr != null) {
                 flat.put(pointType, arr);
@@ -1357,7 +1357,7 @@ public class VideoSearchServiceImpl implements VideoSearchService {
     private JSONArray buildDefaultContribution(JSONObject normalized) {
         JSONArray result = new JSONArray();
         Set<String> seen = new HashSet<>();
-        for (String pointType : new String[]{"灵感点", "关键点", "目的点"}) {
+        for (String pointType : PointTypeEnum.getAllValueList()) {
             JSONArray points = normalized.getJSONArray(pointType);
             if (points == null) continue;
             for (int i = 0; i < points.size(); i++) {

+ 19 - 34
core/src/main/java/com/tzld/videoVector/service/recall/impl/VectorRecallTestServiceImpl.java

@@ -7,7 +7,10 @@ import java.util.function.ToDoubleFunction;
 import com.alibaba.fastjson.JSONObject;
 import com.tzld.videoVector.api.VideoApiService;
 import com.tzld.videoVector.common.constant.VectorConstants;
+import com.tzld.videoVector.common.enums.ContentTypeEnum;
 import com.tzld.videoVector.common.enums.Modality;
+import com.tzld.videoVector.common.enums.PointTypeEnum;
+import com.tzld.videoVector.common.enums.SourceTypeEnum;
 import com.tzld.videoVector.dao.mapper.pgVector.DeconstructVectorConfigMapper;
 import com.tzld.videoVector.dao.mapper.pgVector.ext.AdsMaterialTouliuAllChannelMapperExt;
 import com.tzld.videoVector.dao.mapper.pgVector.ext.ArticleDeconstructResultMapperExt;
@@ -135,14 +138,7 @@ public class VectorRecallTestServiceImpl implements VectorRecallTestService {
     @Value("${video.detail.metrics.days:7}")
     private int metricsDays;
 
-    private static final String SOURCE_AIGC = "aigc_deconstruct";
-    private static final String SOURCE_ODPS_TEXT = "odps_text";
-
-    /** source_type → 中文来源标签 */
-    private static final short SOURCE_TYPE_EXTERNAL = 1;
-    private static final short SOURCE_TYPE_INTERNAL = 2;
-    private static final String SOURCE_LABEL_EXTERNAL = "外部合作";
-    private static final String SOURCE_LABEL_INTERNAL = "内部素材";
+    private static final String PLACEHOLDER = "--";
 
     /** 并行召回线程池:视频和素材召回独立异步执行 */
     private static final ExecutorService RECALL_EXECUTOR = new ThreadPoolExecutor(
@@ -163,15 +159,6 @@ public class VectorRecallTestServiceImpl implements VectorRecallTestService {
         }
     }
 
-    private static final String PLACEHOLDER = "--";
-
-    /**
-     * Redis Key: recall:vid_decode:{vid}
-     * Value: 本地脚本(script/sync_decode_to_redis.py)解析后的瘦身 JSON,
-     *        含 vid/title/videoUrl/htmlUrl/topic/highValuePoints
-     */
-    private static final String REDIS_KEY_DECODE_PREFIX = "recall:vid_decode:";
-
     @Override
     public VideoBasicVO getVideoDetail(Long videoId) {
         if (videoId == null || videoId <= 0L) {
@@ -375,9 +362,9 @@ public class VectorRecallTestServiceImpl implements VectorRecallTestService {
         for (String configCode : configCodes) {
             final String cc = configCode;
             Short ct = ctByCode.get(cc);
-            boolean doVideo = needVideo && appliesToModality(ct, (short) 3);
-            boolean doMaterial = needMaterial && appliesToModality(ct, (short) 2);
-            boolean doArticle = needArticle && appliesToModality(ct, (short) 1);
+            boolean doVideo = needVideo && appliesToModality(ct, ContentTypeEnum.VIDEO.getCode());
+            boolean doMaterial = needMaterial && appliesToModality(ct, ContentTypeEnum.IMAGE_TEXT.getCode());
+            boolean doArticle = needArticle && appliesToModality(ct, ContentTypeEnum.ARTICLE.getCode());
 
             if (doVideo) {
                 futures.add(CompletableFuture.runAsync(() -> {
@@ -1029,11 +1016,11 @@ public class VectorRecallTestServiceImpl implements VectorRecallTestService {
         if (sourceType == null) {
             return null;
         }
-        if (sourceType == SOURCE_TYPE_EXTERNAL) {
-            return SOURCE_LABEL_EXTERNAL;
+        if (sourceType == SourceTypeEnum.EXTERNAL.getCode()) {
+            return SourceTypeEnum.EXTERNAL.getLabel();
         }
-        if (sourceType == SOURCE_TYPE_INTERNAL) {
-            return SOURCE_LABEL_INTERNAL;
+        if (sourceType == SourceTypeEnum.INTERNAL.getCode()) {
+            return SourceTypeEnum.INTERNAL.getLabel();
         }
         return null;
     }
@@ -1237,7 +1224,7 @@ public class VectorRecallTestServiceImpl implements VectorRecallTestService {
         Map<String, ArticleDeconstructResult> result = new HashMap<>();
         try {
             List<ArticleDeconstructResult> rows = articleDeconstructResultMapperExt
-                    .selectResultsByArticleIds(SOURCE_AIGC, articleIds);
+                    .selectResultsByArticleIds(VectorConstants.SOURCE_AIGC, articleIds);
             if (!CollectionUtils.isEmpty(rows)) {
                 for (ArticleDeconstructResult row : rows) {
                     if (row == null || !StringUtils.hasText(row.getArticleId())) {
@@ -1249,7 +1236,7 @@ public class VectorRecallTestServiceImpl implements VectorRecallTestService {
 
             // 补充 ODPS 标题/摘要到 AIGC 解构 JSON 中
             List<ArticleDeconstructResult> odpsRows = articleDeconstructResultMapperExt
-                    .selectResultsByArticleIds(SOURCE_ODPS_TEXT, articleIds);
+                    .selectResultsByArticleIds(VectorConstants.SOURCE_ODPS_TEXT, articleIds);
             if (!CollectionUtils.isEmpty(odpsRows)) {
                 for (ArticleDeconstructResult odpsRow : odpsRows) {
                     if (odpsRow == null || !StringUtils.hasText(odpsRow.getArticleId())) {
@@ -1297,7 +1284,7 @@ public class VectorRecallTestServiceImpl implements VectorRecallTestService {
                             minimal.put("target_post", targetPost);
                             ArticleDeconstructResult newRow = new ArticleDeconstructResult();
                             newRow.setArticleId(odpsRow.getArticleId());
-                            newRow.setSource(SOURCE_AIGC);
+                            newRow.setSource(VectorConstants.SOURCE_AIGC);
                             newRow.setResult(minimal.toJSONString());
                             result.put(odpsRow.getArticleId(), newRow);
                         }
@@ -1410,7 +1397,7 @@ public class VectorRecallTestServiceImpl implements VectorRecallTestService {
         Map<String, MaterialDeconstructResult> result = new HashMap<>();
         try {
             List<MaterialDeconstructResult> rows = materialDeconstructResultMapperExt
-                    .selectResultsByMaterialIds(SOURCE_AIGC, materialIds);
+                    .selectResultsByMaterialIds(VectorConstants.SOURCE_AIGC, materialIds);
             if (CollectionUtils.isEmpty(rows)) {
                 return result;
             }
@@ -1692,7 +1679,7 @@ public class VectorRecallTestServiceImpl implements VectorRecallTestService {
 
         Map<String, List<String>> nameByType = new LinkedHashMap<>();
         Map<String, List<Map<String, Object>>> essenceByType = new LinkedHashMap<>();
-        for (String t : new String[]{"灵感点", "关键点", "目的点"}) {
+        for (String t : PointTypeEnum.getAllValueList()) {
             nameByType.put(t, new ArrayList<>());
             essenceByType.put(t, new ArrayList<>());
         }
@@ -1728,7 +1715,7 @@ public class VectorRecallTestServiceImpl implements VectorRecallTestService {
             }
         }
 
-        for (String t : new String[]{"灵感点", "关键点", "目的点"}) {
+        for (String t : PointTypeEnum.getAllValueList()) {
             out.put(t, nameByType.get(t));
             out.put(t + "-实质", essenceByType.get(t));
         }
@@ -1747,9 +1734,7 @@ public class VectorRecallTestServiceImpl implements VectorRecallTestService {
     private static Short toSourceType(List<String> sourceLabels) {
         if (sourceLabels == null || sourceLabels.size() != 1) return null;
         String label = sourceLabels.get(0);
-        if ("内部素材".equals(label)) return SOURCE_TYPE_INTERNAL;
-        if ("外部合作".equals(label)) return SOURCE_TYPE_EXTERNAL;
-        return null;
+        return SourceTypeEnum.getCodeByLabel(label);
     }
 
     /**
@@ -2654,7 +2639,7 @@ public class VectorRecallTestServiceImpl implements VectorRecallTestService {
             return null;
         }
         try {
-            String json = stringRedisTemplate.opsForValue().get(REDIS_KEY_DECODE_PREFIX + videoId);
+            String json = stringRedisTemplate.opsForValue().get(VectorConstants.VID_DECODE_KEY_PREFIX + videoId);
             if (!StringUtils.hasText(json)) {
                 log.info("getDeconstructPoints: Redis 无 vid={} 的解构缓存", videoId);
                 return null;