瀏覽代碼

接入pgvector素材向量化

wangyunpeng 1 周之前
父節點
當前提交
59b1f6d8ef
共有 28 個文件被更改,包括 1016 次插入602 次删除
  1. 7 5
      core/src/main/java/com/tzld/videoVector/config/db/PgVectorDBConfig.java
  2. 8 8
      core/src/main/java/com/tzld/videoVector/config/db/VideoVectorDBConfig.java
  3. 18 17
      core/src/main/java/com/tzld/videoVector/dao/mapper/videoVector/deconstruct/MysqlDeconstructContentMapper.java
  4. 18 17
      core/src/main/java/com/tzld/videoVector/dao/mapper/videoVector/deconstruct/MysqlDeconstructContentVectorMapper.java
  5. 14 14
      core/src/main/java/com/tzld/videoVector/dao/mapper/videoVector/deconstruct/MysqlDeconstructVectorConfigMapper.java
  6. 418 0
      core/src/main/java/com/tzld/videoVector/job/DataMigrationJob.java
  7. 22 180
      core/src/main/java/com/tzld/videoVector/job/VideoVectorJob.java
  8. 1 1
      core/src/main/java/com/tzld/videoVector/model/po/videoVector/deconstruct/MysqlDeconstructContent.java
  9. 2 2
      core/src/main/java/com/tzld/videoVector/model/po/videoVector/deconstruct/MysqlDeconstructContentExample.java
  10. 1 1
      core/src/main/java/com/tzld/videoVector/model/po/videoVector/deconstruct/MysqlDeconstructContentVector.java
  11. 2 2
      core/src/main/java/com/tzld/videoVector/model/po/videoVector/deconstruct/MysqlDeconstructContentVectorExample.java
  12. 1 1
      core/src/main/java/com/tzld/videoVector/model/po/videoVector/deconstruct/MysqlDeconstructVectorConfig.java
  13. 2 2
      core/src/main/java/com/tzld/videoVector/model/po/videoVector/deconstruct/MysqlDeconstructVectorConfigExample.java
  14. 29 0
      core/src/main/java/com/tzld/videoVector/model/vo/VideoMatchResult.java
  15. 7 7
      core/src/main/java/com/tzld/videoVector/service/VectorizeService.java
  16. 3 1
      core/src/main/java/com/tzld/videoVector/service/VideoSearchService.java
  17. 3 29
      core/src/main/java/com/tzld/videoVector/service/impl/PgVectorStoreServiceImpl.java
  18. 26 206
      core/src/main/java/com/tzld/videoVector/service/impl/VectorizeServiceImpl.java
  19. 42 67
      core/src/main/java/com/tzld/videoVector/service/impl/VideoSearchServiceImpl.java
  20. 281 0
      core/src/main/java/com/tzld/videoVector/util/VectorUtils.java
  21. 2 2
      core/src/main/resources/generator/mybatis-vector-generator-config.xml
  22. 12 12
      core/src/main/resources/mapper/videoVector/deconstruct/MysqlDeconstructContentMapper.xml
  23. 12 12
      core/src/main/resources/mapper/videoVector/deconstruct/MysqlDeconstructContentVectorMapper.xml
  24. 9 9
      core/src/main/resources/mapper/videoVector/deconstruct/MysqlDeconstructVectorConfigMapper.xml
  25. 0 2
      server/src/main/java/com/tzld/videoVector/Application.java
  26. 2 1
      server/src/main/java/com/tzld/videoVector/controller/VideoSearchController.java
  27. 1 4
      server/src/main/resources/application.yml
  28. 73 0
      server/src/test/java/DataMigrationTest.java

+ 7 - 5
core/src/main/java/com/tzld/videoVector/config/db/PgVectorDBConfig.java

@@ -3,7 +3,6 @@ package com.tzld.videoVector.config.db;
 import com.zaxxer.hikari.HikariDataSource;
 import com.zaxxer.hikari.HikariDataSource;
 import org.apache.ibatis.session.SqlSessionFactory;
 import org.apache.ibatis.session.SqlSessionFactory;
 import org.mybatis.spring.SqlSessionFactoryBean;
 import org.mybatis.spring.SqlSessionFactoryBean;
-import org.mybatis.spring.boot.autoconfigure.MybatisProperties;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.boot.context.properties.ConfigurationProperties;
 import org.springframework.boot.context.properties.ConfigurationProperties;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Bean;
@@ -26,11 +25,10 @@ public class PgVectorDBConfig {
         return new HikariDataSource();
         return new HikariDataSource();
     }
     }
 
 
-    // 2. 配置 pgVector 专属 SqlSessionFactory
+    // 2. 配置 pgVector 专属 SqlSessionFactory(独立 Configuration,避免与 MySQL 数据源互相污染)
     @Bean(name = "pgVectorSqlSessionFactory")
     @Bean(name = "pgVectorSqlSessionFactory")
     public SqlSessionFactory pgVectorSqlSessionFactory(
     public SqlSessionFactory pgVectorSqlSessionFactory(
-            @Qualifier("pgVectorDataSource") DataSource pgVectorDataSource,
-            MybatisProperties properties) throws Exception {
+            @Qualifier("pgVectorDataSource") DataSource pgVectorDataSource) throws Exception {
         SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
         SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
         sessionFactory.setDataSource(pgVectorDataSource);
         sessionFactory.setDataSource(pgVectorDataSource);
 
 
@@ -45,8 +43,12 @@ public class PgVectorDBConfig {
             // 如果目录不存在或为空,不设置 mapper locations,应用仍可正常启动
             // 如果目录不存在或为空,不设置 mapper locations,应用仍可正常启动
         }
         }
 
 
+        // 每个数据源必须独立创建 Configuration,不能共享 MybatisProperties.getConfiguration()
+        org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
+        configuration.setMapUnderscoreToCamelCase(true);
+        configuration.setUseGeneratedKeys(true);
+        sessionFactory.setConfiguration(configuration);
         sessionFactory.setTypeAliasesPackage("com.tzld.videoVector");
         sessionFactory.setTypeAliasesPackage("com.tzld.videoVector");
-        sessionFactory.setConfiguration(properties.getConfiguration());
         return sessionFactory.getObject();
         return sessionFactory.getObject();
     }
     }
 
 

+ 8 - 8
core/src/main/java/com/tzld/videoVector/config/db/VideoVectorDBConfig.java

@@ -3,7 +3,6 @@ package com.tzld.videoVector.config.db;
 import com.zaxxer.hikari.HikariDataSource;
 import com.zaxxer.hikari.HikariDataSource;
 import org.apache.ibatis.session.SqlSessionFactory;
 import org.apache.ibatis.session.SqlSessionFactory;
 import org.mybatis.spring.SqlSessionFactoryBean;
 import org.mybatis.spring.SqlSessionFactoryBean;
-import org.mybatis.spring.boot.autoconfigure.MybatisProperties;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.boot.context.properties.ConfigurationProperties;
 import org.springframework.boot.context.properties.ConfigurationProperties;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Bean;
@@ -28,17 +27,15 @@ public class VideoVectorDBConfig {
         return new HikariDataSource(); // 使用 HikariCP 连接池
         return new HikariDataSource(); // 使用 HikariCP 连接池
     }
     }
 
 
-    // 2. 配置 videoVector 专属 SqlSessionFactory
+    // 2. 配置 videoVector 专属 SqlSessionFactory(独立 Configuration,避免与 PG 数据源互相污染)
     @Primary
     @Primary
     @Bean(name = "videoVectorSqlSessionFactory")
     @Bean(name = "videoVectorSqlSessionFactory")
     public SqlSessionFactory videoVectorSqlSessionFactory(
     public SqlSessionFactory videoVectorSqlSessionFactory(
-            @Qualifier("videoVectorDataSource") DataSource videoVectorDataSource,
-            MybatisProperties properties) throws Exception {
+            @Qualifier("videoVectorDataSource") DataSource videoVectorDataSource) throws Exception {
         SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
         SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
         sessionFactory.setDataSource(videoVectorDataSource); // 关联 videoVector 数据源
         sessionFactory.setDataSource(videoVectorDataSource); // 关联 videoVector 数据源
         
         
         // 关键:指定 videoVector 模块的 mapper 文件路径(隔离其他数据源)
         // 关键:指定 videoVector 模块的 mapper 文件路径(隔离其他数据源)
-        // 使用 try-catch 避免空目录报错
         try {
         try {
             org.springframework.core.io.Resource[] mapperResources = 
             org.springframework.core.io.Resource[] mapperResources = 
                 new PathMatchingResourcePatternResolver().getResources("classpath:mapper/videoVector/**/*.xml");
                 new PathMatchingResourcePatternResolver().getResources("classpath:mapper/videoVector/**/*.xml");
@@ -46,12 +43,15 @@ public class VideoVectorDBConfig {
                 sessionFactory.setMapperLocations(mapperResources);
                 sessionFactory.setMapperLocations(mapperResources);
             }
             }
         } catch (Exception e) {
         } catch (Exception e) {
-            // 如果目录不存在或为空,不设置 mapper  locations,应用仍可正常启动
-            // 后续添加 mapper 文件后重启即可生效
+            // 如果目录不存在或为空,不设置 mapper locations,应用仍可正常启动
         }
         }
         
         
+        // 每个数据源必须独立创建 Configuration,不能共享 MybatisProperties.getConfiguration()
+        org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
+        configuration.setMapUnderscoreToCamelCase(true);
+        configuration.setUseGeneratedKeys(true);
+        sessionFactory.setConfiguration(configuration);
         sessionFactory.setTypeAliasesPackage("com.tzld.videoVector");
         sessionFactory.setTypeAliasesPackage("com.tzld.videoVector");
-        sessionFactory.setConfiguration(properties.getConfiguration());
         return sessionFactory.getObject();
         return sessionFactory.getObject();
     }
     }
 
 

+ 18 - 17
core/src/main/java/com/tzld/videoVector/dao/mapper/videoVector/deconstruct/DeconstructContentMapper.java → core/src/main/java/com/tzld/videoVector/dao/mapper/videoVector/deconstruct/MysqlDeconstructContentMapper.java

@@ -1,18 +1,19 @@
 package com.tzld.videoVector.dao.mapper.videoVector.deconstruct;
 package com.tzld.videoVector.dao.mapper.videoVector.deconstruct;
 
 
-import com.tzld.videoVector.model.po.videoVector.deconstruct.DeconstructContent;
-import com.tzld.videoVector.model.po.videoVector.deconstruct.DeconstructContentExample;
-import java.util.List;
+import com.tzld.videoVector.model.po.videoVector.deconstruct.MysqlDeconstructContent;
+import com.tzld.videoVector.model.po.videoVector.deconstruct.MysqlDeconstructContentExample;
 import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Param;
 
 
-public interface DeconstructContentMapper {
+import java.util.List;
+
+public interface MysqlDeconstructContentMapper {
     /**
     /**
      * This method was generated by MyBatis Generator.
      * This method was generated by MyBatis Generator.
      * This method corresponds to the database table deconstruct_content
      * This method corresponds to the database table deconstruct_content
      *
      *
      * @mbg.generated Mon Mar 09 10:39:51 CST 2026
      * @mbg.generated Mon Mar 09 10:39:51 CST 2026
      */
      */
-    long countByExample(DeconstructContentExample example);
+    long countByExample(MysqlDeconstructContentExample example);
 
 
     /**
     /**
      * This method was generated by MyBatis Generator.
      * This method was generated by MyBatis Generator.
@@ -20,7 +21,7 @@ public interface DeconstructContentMapper {
      *
      *
      * @mbg.generated Mon Mar 09 10:39:51 CST 2026
      * @mbg.generated Mon Mar 09 10:39:51 CST 2026
      */
      */
-    int deleteByExample(DeconstructContentExample example);
+    int deleteByExample(MysqlDeconstructContentExample example);
 
 
     /**
     /**
      * This method was generated by MyBatis Generator.
      * This method was generated by MyBatis Generator.
@@ -36,7 +37,7 @@ public interface DeconstructContentMapper {
      *
      *
      * @mbg.generated Mon Mar 09 10:39:51 CST 2026
      * @mbg.generated Mon Mar 09 10:39:51 CST 2026
      */
      */
-    int insert(DeconstructContent record);
+    int insert(MysqlDeconstructContent record);
 
 
     /**
     /**
      * This method was generated by MyBatis Generator.
      * This method was generated by MyBatis Generator.
@@ -44,7 +45,7 @@ public interface DeconstructContentMapper {
      *
      *
      * @mbg.generated Mon Mar 09 10:39:51 CST 2026
      * @mbg.generated Mon Mar 09 10:39:51 CST 2026
      */
      */
-    int insertSelective(DeconstructContent record);
+    int insertSelective(MysqlDeconstructContent record);
 
 
     /**
     /**
      * This method was generated by MyBatis Generator.
      * This method was generated by MyBatis Generator.
@@ -52,7 +53,7 @@ public interface DeconstructContentMapper {
      *
      *
      * @mbg.generated Mon Mar 09 10:39:51 CST 2026
      * @mbg.generated Mon Mar 09 10:39:51 CST 2026
      */
      */
-    List<DeconstructContent> selectByExampleWithBLOBs(DeconstructContentExample example);
+    List<MysqlDeconstructContent> selectByExampleWithBLOBs(MysqlDeconstructContentExample example);
 
 
     /**
     /**
      * This method was generated by MyBatis Generator.
      * This method was generated by MyBatis Generator.
@@ -60,7 +61,7 @@ public interface DeconstructContentMapper {
      *
      *
      * @mbg.generated Mon Mar 09 10:39:51 CST 2026
      * @mbg.generated Mon Mar 09 10:39:51 CST 2026
      */
      */
-    List<DeconstructContent> selectByExample(DeconstructContentExample example);
+    List<MysqlDeconstructContent> selectByExample(MysqlDeconstructContentExample example);
 
 
     /**
     /**
      * This method was generated by MyBatis Generator.
      * This method was generated by MyBatis Generator.
@@ -68,7 +69,7 @@ public interface DeconstructContentMapper {
      *
      *
      * @mbg.generated Mon Mar 09 10:39:51 CST 2026
      * @mbg.generated Mon Mar 09 10:39:51 CST 2026
      */
      */
-    DeconstructContent selectByPrimaryKey(Long id);
+    MysqlDeconstructContent selectByPrimaryKey(Long id);
 
 
     /**
     /**
      * This method was generated by MyBatis Generator.
      * This method was generated by MyBatis Generator.
@@ -76,7 +77,7 @@ public interface DeconstructContentMapper {
      *
      *
      * @mbg.generated Mon Mar 09 10:39:51 CST 2026
      * @mbg.generated Mon Mar 09 10:39:51 CST 2026
      */
      */
-    int updateByExampleSelective(@Param("record") DeconstructContent record, @Param("example") DeconstructContentExample example);
+    int updateByExampleSelective(@Param("record") MysqlDeconstructContent record, @Param("example") MysqlDeconstructContentExample example);
 
 
     /**
     /**
      * This method was generated by MyBatis Generator.
      * This method was generated by MyBatis Generator.
@@ -84,7 +85,7 @@ public interface DeconstructContentMapper {
      *
      *
      * @mbg.generated Mon Mar 09 10:39:51 CST 2026
      * @mbg.generated Mon Mar 09 10:39:51 CST 2026
      */
      */
-    int updateByExampleWithBLOBs(@Param("record") DeconstructContent record, @Param("example") DeconstructContentExample example);
+    int updateByExampleWithBLOBs(@Param("record") MysqlDeconstructContent record, @Param("example") MysqlDeconstructContentExample example);
 
 
     /**
     /**
      * This method was generated by MyBatis Generator.
      * This method was generated by MyBatis Generator.
@@ -92,7 +93,7 @@ public interface DeconstructContentMapper {
      *
      *
      * @mbg.generated Mon Mar 09 10:39:51 CST 2026
      * @mbg.generated Mon Mar 09 10:39:51 CST 2026
      */
      */
-    int updateByExample(@Param("record") DeconstructContent record, @Param("example") DeconstructContentExample example);
+    int updateByExample(@Param("record") MysqlDeconstructContent record, @Param("example") MysqlDeconstructContentExample example);
 
 
     /**
     /**
      * This method was generated by MyBatis Generator.
      * This method was generated by MyBatis Generator.
@@ -100,7 +101,7 @@ public interface DeconstructContentMapper {
      *
      *
      * @mbg.generated Mon Mar 09 10:39:51 CST 2026
      * @mbg.generated Mon Mar 09 10:39:51 CST 2026
      */
      */
-    int updateByPrimaryKeySelective(DeconstructContent record);
+    int updateByPrimaryKeySelective(MysqlDeconstructContent record);
 
 
     /**
     /**
      * This method was generated by MyBatis Generator.
      * This method was generated by MyBatis Generator.
@@ -108,7 +109,7 @@ public interface DeconstructContentMapper {
      *
      *
      * @mbg.generated Mon Mar 09 10:39:51 CST 2026
      * @mbg.generated Mon Mar 09 10:39:51 CST 2026
      */
      */
-    int updateByPrimaryKeyWithBLOBs(DeconstructContent record);
+    int updateByPrimaryKeyWithBLOBs(MysqlDeconstructContent record);
 
 
     /**
     /**
      * This method was generated by MyBatis Generator.
      * This method was generated by MyBatis Generator.
@@ -116,5 +117,5 @@ public interface DeconstructContentMapper {
      *
      *
      * @mbg.generated Mon Mar 09 10:39:51 CST 2026
      * @mbg.generated Mon Mar 09 10:39:51 CST 2026
      */
      */
-    int updateByPrimaryKey(DeconstructContent record);
+    int updateByPrimaryKey(MysqlDeconstructContent record);
 }
 }

+ 18 - 17
core/src/main/java/com/tzld/videoVector/dao/mapper/videoVector/deconstruct/DeconstructContentVectorMapper.java → core/src/main/java/com/tzld/videoVector/dao/mapper/videoVector/deconstruct/MysqlDeconstructContentVectorMapper.java

@@ -1,18 +1,19 @@
 package com.tzld.videoVector.dao.mapper.videoVector.deconstruct;
 package com.tzld.videoVector.dao.mapper.videoVector.deconstruct;
 
 
-import com.tzld.videoVector.model.po.videoVector.deconstruct.DeconstructContentVector;
-import com.tzld.videoVector.model.po.videoVector.deconstruct.DeconstructContentVectorExample;
-import java.util.List;
+import com.tzld.videoVector.model.po.videoVector.deconstruct.MysqlDeconstructContentVector;
+import com.tzld.videoVector.model.po.videoVector.deconstruct.MysqlDeconstructContentVectorExample;
 import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Param;
 
 
-public interface DeconstructContentVectorMapper {
+import java.util.List;
+
+public interface MysqlDeconstructContentVectorMapper {
     /**
     /**
      * This method was generated by MyBatis Generator.
      * This method was generated by MyBatis Generator.
      * This method corresponds to the database table deconstruct_content_vector
      * This method corresponds to the database table deconstruct_content_vector
      *
      *
      * @mbg.generated Thu Apr 23 15:03:47 CST 2026
      * @mbg.generated Thu Apr 23 15:03:47 CST 2026
      */
      */
-    long countByExample(DeconstructContentVectorExample example);
+    long countByExample(MysqlDeconstructContentVectorExample example);
 
 
     /**
     /**
      * This method was generated by MyBatis Generator.
      * This method was generated by MyBatis Generator.
@@ -20,7 +21,7 @@ public interface DeconstructContentVectorMapper {
      *
      *
      * @mbg.generated Thu Apr 23 15:03:47 CST 2026
      * @mbg.generated Thu Apr 23 15:03:47 CST 2026
      */
      */
-    int deleteByExample(DeconstructContentVectorExample example);
+    int deleteByExample(MysqlDeconstructContentVectorExample example);
 
 
     /**
     /**
      * This method was generated by MyBatis Generator.
      * This method was generated by MyBatis Generator.
@@ -36,7 +37,7 @@ public interface DeconstructContentVectorMapper {
      *
      *
      * @mbg.generated Thu Apr 23 15:03:47 CST 2026
      * @mbg.generated Thu Apr 23 15:03:47 CST 2026
      */
      */
-    int insert(DeconstructContentVector record);
+    int insert(MysqlDeconstructContentVector record);
 
 
     /**
     /**
      * This method was generated by MyBatis Generator.
      * This method was generated by MyBatis Generator.
@@ -44,7 +45,7 @@ public interface DeconstructContentVectorMapper {
      *
      *
      * @mbg.generated Thu Apr 23 15:03:47 CST 2026
      * @mbg.generated Thu Apr 23 15:03:47 CST 2026
      */
      */
-    int insertSelective(DeconstructContentVector record);
+    int insertSelective(MysqlDeconstructContentVector record);
 
 
     /**
     /**
      * This method was generated by MyBatis Generator.
      * This method was generated by MyBatis Generator.
@@ -52,7 +53,7 @@ public interface DeconstructContentVectorMapper {
      *
      *
      * @mbg.generated Thu Apr 23 15:03:47 CST 2026
      * @mbg.generated Thu Apr 23 15:03:47 CST 2026
      */
      */
-    List<DeconstructContentVector> selectByExampleWithBLOBs(DeconstructContentVectorExample example);
+    List<MysqlDeconstructContentVector> selectByExampleWithBLOBs(MysqlDeconstructContentVectorExample example);
 
 
     /**
     /**
      * This method was generated by MyBatis Generator.
      * This method was generated by MyBatis Generator.
@@ -60,7 +61,7 @@ public interface DeconstructContentVectorMapper {
      *
      *
      * @mbg.generated Thu Apr 23 15:03:47 CST 2026
      * @mbg.generated Thu Apr 23 15:03:47 CST 2026
      */
      */
-    List<DeconstructContentVector> selectByExample(DeconstructContentVectorExample example);
+    List<MysqlDeconstructContentVector> selectByExample(MysqlDeconstructContentVectorExample example);
 
 
     /**
     /**
      * This method was generated by MyBatis Generator.
      * This method was generated by MyBatis Generator.
@@ -68,7 +69,7 @@ public interface DeconstructContentVectorMapper {
      *
      *
      * @mbg.generated Thu Apr 23 15:03:47 CST 2026
      * @mbg.generated Thu Apr 23 15:03:47 CST 2026
      */
      */
-    DeconstructContentVector selectByPrimaryKey(Long id);
+    MysqlDeconstructContentVector selectByPrimaryKey(Long id);
 
 
     /**
     /**
      * This method was generated by MyBatis Generator.
      * This method was generated by MyBatis Generator.
@@ -76,7 +77,7 @@ public interface DeconstructContentVectorMapper {
      *
      *
      * @mbg.generated Thu Apr 23 15:03:47 CST 2026
      * @mbg.generated Thu Apr 23 15:03:47 CST 2026
      */
      */
-    int updateByExampleSelective(@Param("record") DeconstructContentVector record, @Param("example") DeconstructContentVectorExample example);
+    int updateByExampleSelective(@Param("record") MysqlDeconstructContentVector record, @Param("example") MysqlDeconstructContentVectorExample example);
 
 
     /**
     /**
      * This method was generated by MyBatis Generator.
      * This method was generated by MyBatis Generator.
@@ -84,7 +85,7 @@ public interface DeconstructContentVectorMapper {
      *
      *
      * @mbg.generated Thu Apr 23 15:03:47 CST 2026
      * @mbg.generated Thu Apr 23 15:03:47 CST 2026
      */
      */
-    int updateByExampleWithBLOBs(@Param("record") DeconstructContentVector record, @Param("example") DeconstructContentVectorExample example);
+    int updateByExampleWithBLOBs(@Param("record") MysqlDeconstructContentVector record, @Param("example") MysqlDeconstructContentVectorExample example);
 
 
     /**
     /**
      * This method was generated by MyBatis Generator.
      * This method was generated by MyBatis Generator.
@@ -92,7 +93,7 @@ public interface DeconstructContentVectorMapper {
      *
      *
      * @mbg.generated Thu Apr 23 15:03:47 CST 2026
      * @mbg.generated Thu Apr 23 15:03:47 CST 2026
      */
      */
-    int updateByExample(@Param("record") DeconstructContentVector record, @Param("example") DeconstructContentVectorExample example);
+    int updateByExample(@Param("record") MysqlDeconstructContentVector record, @Param("example") MysqlDeconstructContentVectorExample example);
 
 
     /**
     /**
      * This method was generated by MyBatis Generator.
      * This method was generated by MyBatis Generator.
@@ -100,7 +101,7 @@ public interface DeconstructContentVectorMapper {
      *
      *
      * @mbg.generated Thu Apr 23 15:03:47 CST 2026
      * @mbg.generated Thu Apr 23 15:03:47 CST 2026
      */
      */
-    int updateByPrimaryKeySelective(DeconstructContentVector record);
+    int updateByPrimaryKeySelective(MysqlDeconstructContentVector record);
 
 
     /**
     /**
      * This method was generated by MyBatis Generator.
      * This method was generated by MyBatis Generator.
@@ -108,7 +109,7 @@ public interface DeconstructContentVectorMapper {
      *
      *
      * @mbg.generated Thu Apr 23 15:03:47 CST 2026
      * @mbg.generated Thu Apr 23 15:03:47 CST 2026
      */
      */
-    int updateByPrimaryKeyWithBLOBs(DeconstructContentVector record);
+    int updateByPrimaryKeyWithBLOBs(MysqlDeconstructContentVector record);
 
 
     /**
     /**
      * This method was generated by MyBatis Generator.
      * This method was generated by MyBatis Generator.
@@ -116,5 +117,5 @@ public interface DeconstructContentVectorMapper {
      *
      *
      * @mbg.generated Thu Apr 23 15:03:47 CST 2026
      * @mbg.generated Thu Apr 23 15:03:47 CST 2026
      */
      */
-    int updateByPrimaryKey(DeconstructContentVector record);
+    int updateByPrimaryKey(MysqlDeconstructContentVector record);
 }
 }

+ 14 - 14
core/src/main/java/com/tzld/videoVector/dao/mapper/videoVector/deconstruct/DeconstructVectorConfigMapper.java → core/src/main/java/com/tzld/videoVector/dao/mapper/videoVector/deconstruct/MysqlDeconstructVectorConfigMapper.java

@@ -1,18 +1,18 @@
 package com.tzld.videoVector.dao.mapper.videoVector.deconstruct;
 package com.tzld.videoVector.dao.mapper.videoVector.deconstruct;
 
 
-import com.tzld.videoVector.model.po.videoVector.deconstruct.DeconstructVectorConfig;
-import com.tzld.videoVector.model.po.videoVector.deconstruct.DeconstructVectorConfigExample;
+import com.tzld.videoVector.model.po.videoVector.deconstruct.MysqlDeconstructVectorConfig;
+import com.tzld.videoVector.model.po.videoVector.deconstruct.MysqlDeconstructVectorConfigExample;
 import java.util.List;
 import java.util.List;
 import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Param;
 
 
-public interface DeconstructVectorConfigMapper {
+public interface MysqlDeconstructVectorConfigMapper {
     /**
     /**
      * This method was generated by MyBatis Generator.
      * This method was generated by MyBatis Generator.
      * This method corresponds to the database table deconstruct_vector_config
      * This method corresponds to the database table deconstruct_vector_config
      *
      *
      * @mbg.generated Thu Apr 23 14:55:06 CST 2026
      * @mbg.generated Thu Apr 23 14:55:06 CST 2026
      */
      */
-    long countByExample(DeconstructVectorConfigExample example);
+    long countByExample(MysqlDeconstructVectorConfigExample example);
 
 
     /**
     /**
      * This method was generated by MyBatis Generator.
      * This method was generated by MyBatis Generator.
@@ -20,7 +20,7 @@ public interface DeconstructVectorConfigMapper {
      *
      *
      * @mbg.generated Thu Apr 23 14:55:06 CST 2026
      * @mbg.generated Thu Apr 23 14:55:06 CST 2026
      */
      */
-    int deleteByExample(DeconstructVectorConfigExample example);
+    int deleteByExample(MysqlDeconstructVectorConfigExample example);
 
 
     /**
     /**
      * This method was generated by MyBatis Generator.
      * This method was generated by MyBatis Generator.
@@ -36,7 +36,7 @@ public interface DeconstructVectorConfigMapper {
      *
      *
      * @mbg.generated Thu Apr 23 14:55:06 CST 2026
      * @mbg.generated Thu Apr 23 14:55:06 CST 2026
      */
      */
-    int insert(DeconstructVectorConfig record);
+    int insert(MysqlDeconstructVectorConfig record);
 
 
     /**
     /**
      * This method was generated by MyBatis Generator.
      * This method was generated by MyBatis Generator.
@@ -44,7 +44,7 @@ public interface DeconstructVectorConfigMapper {
      *
      *
      * @mbg.generated Thu Apr 23 14:55:06 CST 2026
      * @mbg.generated Thu Apr 23 14:55:06 CST 2026
      */
      */
-    int insertSelective(DeconstructVectorConfig record);
+    int insertSelective(MysqlDeconstructVectorConfig record);
 
 
     /**
     /**
      * This method was generated by MyBatis Generator.
      * This method was generated by MyBatis Generator.
@@ -52,7 +52,7 @@ public interface DeconstructVectorConfigMapper {
      *
      *
      * @mbg.generated Thu Apr 23 14:55:06 CST 2026
      * @mbg.generated Thu Apr 23 14:55:06 CST 2026
      */
      */
-    List<DeconstructVectorConfig> selectByExample(DeconstructVectorConfigExample example);
+    List<MysqlDeconstructVectorConfig> selectByExample(MysqlDeconstructVectorConfigExample example);
 
 
     /**
     /**
      * This method was generated by MyBatis Generator.
      * This method was generated by MyBatis Generator.
@@ -60,7 +60,7 @@ public interface DeconstructVectorConfigMapper {
      *
      *
      * @mbg.generated Thu Apr 23 14:55:06 CST 2026
      * @mbg.generated Thu Apr 23 14:55:06 CST 2026
      */
      */
-    DeconstructVectorConfig selectByPrimaryKey(Long id);
+    MysqlDeconstructVectorConfig selectByPrimaryKey(Long id);
 
 
     /**
     /**
      * This method was generated by MyBatis Generator.
      * This method was generated by MyBatis Generator.
@@ -68,7 +68,7 @@ public interface DeconstructVectorConfigMapper {
      *
      *
      * @mbg.generated Thu Apr 23 14:55:06 CST 2026
      * @mbg.generated Thu Apr 23 14:55:06 CST 2026
      */
      */
-    int updateByExampleSelective(@Param("record") DeconstructVectorConfig record, @Param("example") DeconstructVectorConfigExample example);
+    int updateByExampleSelective(@Param("record") MysqlDeconstructVectorConfig record, @Param("example") MysqlDeconstructVectorConfigExample example);
 
 
     /**
     /**
      * This method was generated by MyBatis Generator.
      * This method was generated by MyBatis Generator.
@@ -76,7 +76,7 @@ public interface DeconstructVectorConfigMapper {
      *
      *
      * @mbg.generated Thu Apr 23 14:55:06 CST 2026
      * @mbg.generated Thu Apr 23 14:55:06 CST 2026
      */
      */
-    int updateByExample(@Param("record") DeconstructVectorConfig record, @Param("example") DeconstructVectorConfigExample example);
+    int updateByExample(@Param("record") MysqlDeconstructVectorConfig record, @Param("example") MysqlDeconstructVectorConfigExample example);
 
 
     /**
     /**
      * This method was generated by MyBatis Generator.
      * This method was generated by MyBatis Generator.
@@ -84,7 +84,7 @@ public interface DeconstructVectorConfigMapper {
      *
      *
      * @mbg.generated Thu Apr 23 14:55:06 CST 2026
      * @mbg.generated Thu Apr 23 14:55:06 CST 2026
      */
      */
-    int updateByPrimaryKeySelective(DeconstructVectorConfig record);
+    int updateByPrimaryKeySelective(MysqlDeconstructVectorConfig record);
 
 
     /**
     /**
      * This method was generated by MyBatis Generator.
      * This method was generated by MyBatis Generator.
@@ -92,7 +92,7 @@ public interface DeconstructVectorConfigMapper {
      *
      *
      * @mbg.generated Thu Apr 23 14:55:06 CST 2026
      * @mbg.generated Thu Apr 23 14:55:06 CST 2026
      */
      */
-    int updateByPrimaryKey(DeconstructVectorConfig record);
+    int updateByPrimaryKey(MysqlDeconstructVectorConfig record);
 
 
     /**
     /**
      * 按业务类型和内容类型查询启用的向量配置,支持 null 表示通配所有类型
      * 按业务类型和内容类型查询启用的向量配置,支持 null 表示通配所有类型
@@ -102,7 +102,7 @@ public interface DeconstructVectorConfigMapper {
      * @param contentType 内容类型,为 null 时不过滤
      * @param contentType 内容类型,为 null 时不过滤
      * @return 匹配的向量配置列表
      * @return 匹配的向量配置列表
      */
      */
-    List<DeconstructVectorConfig> selectMatchingConfigs(
+    List<MysqlDeconstructVectorConfig> selectMatchingConfigs(
             @Param("bizType") Byte bizType,
             @Param("bizType") Byte bizType,
             @Param("contentType") Byte contentType);
             @Param("contentType") Byte contentType);
 }
 }

+ 418 - 0
core/src/main/java/com/tzld/videoVector/job/DataMigrationJob.java

@@ -0,0 +1,418 @@
+package com.tzld.videoVector.job;
+
+import com.tzld.videoVector.common.constant.VectorConstants;
+import com.tzld.videoVector.dao.mapper.pgVector.ContentVectorMapper;
+import com.tzld.videoVector.dao.mapper.pgVector.DeconstructContentMapper;
+import com.tzld.videoVector.dao.mapper.pgVector.DeconstructVectorConfigMapper;
+import com.tzld.videoVector.dao.mapper.pgVector.VideoVectorMapper;
+import com.tzld.videoVector.dao.mapper.videoVector.deconstruct.MysqlDeconstructContentVectorMapper;
+import com.tzld.videoVector.dao.mapper.videoVector.deconstruct.MysqlDeconstructContentMapper;
+import com.tzld.videoVector.dao.mapper.videoVector.deconstruct.MysqlDeconstructVectorConfigMapper;
+import com.tzld.videoVector.model.po.pgVector.DeconstructContent;
+import com.tzld.videoVector.model.po.pgVector.DeconstructVectorConfig;
+import com.tzld.videoVector.model.po.pgVector.DeconstructVectorConfigExample;
+import com.tzld.videoVector.model.po.videoVector.deconstruct.*;
+import com.xxl.job.core.biz.model.ReturnT;
+import com.xxl.job.core.handler.annotation.XxlJob;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.stereotype.Component;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+import javax.annotation.Resource;
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * 数据迁移任务
+ * 1. MySQL deconstruct_content / deconstruct_vector_config → PG
+ * 2. MySQL deconstruct_content_vector → PG content_vectors
+ * 3. Redis 视频向量缓存 → PG video_vectors
+ */
+@Slf4j
+@Component
+public class DataMigrationJob {
+
+    // ==================== MySQL 数据源 Mapper ====================
+    @Resource
+    private MysqlDeconstructContentMapper mysqlContentMapper;
+
+    @Resource
+    private MysqlDeconstructVectorConfigMapper mysqlConfigMapper;
+
+    @Resource
+    private MysqlDeconstructContentVectorMapper mysqlContentVectorMapper;
+
+    // ==================== PG 数据源 Mapper ====================
+    @Resource
+    private DeconstructContentMapper pgContentMapper;
+
+    @Resource
+    private DeconstructVectorConfigMapper pgConfigMapper;
+
+    @Resource
+    private ContentVectorMapper pgContentVectorMapper;
+
+    @Resource
+    private VideoVectorMapper pgVideoVectorMapper;
+
+    // ==================== Redis ====================
+    @Resource
+    private RedisTemplate<String, Object> redisTemplate;
+
+    // 向量值是纯 JSON 字符串,没有 Jackson 类型信息,需用 StringRedisTemplate 读取
+    @Resource
+    private StringRedisTemplate stringRedisTemplate;
+
+    // ==================== 迁移批次大小 ====================
+    private static final int BATCH_SIZE = 200;
+
+    /**
+     * MySQL deconstruct_content 表迁移到 PG
+     */
+    @XxlJob("migrateContentToPgJob")
+    public ReturnT<String> migrateContentToPgJob(String param) {
+        log.info("开始迁移 deconstruct_content 数据到 PG, param: {}", param);
+
+        try {
+            // 从 MySQL 分页读取
+            MysqlDeconstructContentExample example =
+                    new MysqlDeconstructContentExample();
+            example.setOrderByClause("id ASC");
+            List<MysqlDeconstructContent> allRecords =
+                    mysqlContentMapper.selectByExampleWithBLOBs(example);
+
+            if (CollectionUtils.isEmpty(allRecords)) {
+                log.info("MySQL deconstruct_content 无数据,跳过");
+                return ReturnT.SUCCESS;
+            }
+
+            log.info("查询到 {} 条 deconstruct_content 记录待迁移", allRecords.size());
+
+            int successCount = 0;
+            int skipCount = 0;
+            int failCount = 0;
+
+            for (MysqlDeconstructContent mysql : allRecords) {
+                try {
+                    // 转换为 PG 实体
+                    DeconstructContent pg = convertContent(mysql);
+                    // 插入 PG(使用 insertSelective 避免主键冲突时可手动处理)
+                    pgContentMapper.insertSelective(pg);
+                    successCount++;
+                } catch (Exception e) {
+                    if (e.getMessage() != null && e.getMessage().contains("duplicate key")) {
+                        skipCount++;
+                    } else {
+                        failCount++;
+                        log.error("迁移 content id={} 失败: {}", mysql.getId(), e.getMessage());
+                    }
+                }
+            }
+
+            log.info("deconstruct_content 迁移完成,成功: {}, 跳过(已存在): {}, 失败: {}",
+                    successCount, skipCount, failCount);
+            return ReturnT.SUCCESS;
+        } catch (Exception e) {
+            log.error("deconstruct_content 迁移失败: {}", e.getMessage(), e);
+            return new ReturnT<>(ReturnT.FAIL_CODE, "迁移失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * MySQL deconstruct_vector_config 表迁移到 PG
+     */
+    @XxlJob("migrateVectorConfigToPgJob")
+    public ReturnT<String> migrateVectorConfigToPgJob(String param) {
+        log.info("开始迁移 deconstruct_vector_config 数据到 PG, param: {}", param);
+
+        try {
+            MysqlDeconstructVectorConfigExample example =
+                    new MysqlDeconstructVectorConfigExample();
+            example.setOrderByClause("id ASC");
+            List<MysqlDeconstructVectorConfig> allRecords =
+                    mysqlConfigMapper.selectByExample(example);
+
+            if (CollectionUtils.isEmpty(allRecords)) {
+                log.info("MySQL deconstruct_vector_config 无数据,跳过");
+                return ReturnT.SUCCESS;
+            }
+
+            log.info("查询到 {} 条 deconstruct_vector_config 记录待迁移", allRecords.size());
+
+            int successCount = 0;
+            int skipCount = 0;
+            int failCount = 0;
+
+            for (MysqlDeconstructVectorConfig mysql : allRecords) {
+                try {
+                    DeconstructVectorConfig pg = convertVectorConfig(mysql);
+                    pgConfigMapper.insertSelective(pg);
+                    successCount++;
+                } catch (Exception e) {
+                    if (e.getMessage() != null && e.getMessage().contains("duplicate key")) {
+                        skipCount++;
+                    } else {
+                        failCount++;
+                        log.error("迁移 vector_config id={} 失败: {}", mysql.getId(), e.getMessage());
+                    }
+                }
+            }
+
+            log.info("deconstruct_vector_config 迁移完成,成功: {}, 跳过(已存在): {}, 失败: {}",
+                    successCount, skipCount, failCount);
+            return ReturnT.SUCCESS;
+        } catch (Exception e) {
+            log.error("deconstruct_vector_config 迁移失败: {}", e.getMessage(), e);
+            return new ReturnT<>(ReturnT.FAIL_CODE, "迁移失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * MySQL deconstruct_content_vector 表迁移到 PG content_vectors
+     * 将 vector_data(JSON 数组)转为 pgvector embedding
+     */
+    @XxlJob("migrateContentVectorToPgJob")
+    public ReturnT<String> migrateContentVectorToPgJob(String param) {
+        log.info("开始迁移 deconstruct_content_vector 数据到 PG content_vectors, param: {}", param);
+
+        try {
+            MysqlDeconstructContentVectorExample example = new MysqlDeconstructContentVectorExample();
+            example.setOrderByClause("id ASC");
+            List<MysqlDeconstructContentVector> allRecords = mysqlContentVectorMapper.selectByExampleWithBLOBs(example);
+
+            if (CollectionUtils.isEmpty(allRecords)) {
+                log.info("MySQL deconstruct_content_vector 无数据,跳过");
+                return ReturnT.SUCCESS;
+            }
+
+            log.info("查询到 {} 条 deconstruct_content_vector 记录待迁移", allRecords.size());
+
+            int successCount = 0;
+            int skipCount = 0;
+            int failCount = 0;
+
+            for (MysqlDeconstructContentVector mysql : allRecords) {
+                try {
+                    String vectorData = mysql.getVectorData();
+                    if (!StringUtils.hasText(vectorData)) {
+                        log.debug("content_vector id={} vectorData 为空,跳过", mysql.getId());
+                        skipCount++;
+                        continue;
+                    }
+
+                    // vectorData 是 JSON 数组格式 "[0.1,0.2,...]",pgvector 接受此格式
+                    pgContentVectorMapper.upsertWithEmbedding(
+                            mysql.getContentId(),
+                            mysql.getTaskId(),
+                            mysql.getConfigCode(),
+                            mysql.getSourceField(),
+                            mysql.getSourcePath(),
+                            mysql.getTextHash(),
+                            mysql.getEmbeddingModel(),
+                            mysql.getSegmentIndex(),
+                            mysql.getSegmentTotal(),
+                            mysql.getSourceText(),
+                            vectorData
+                    );
+                    successCount++;
+                } catch (Exception e) {
+                    if (e.getMessage() != null && e.getMessage().contains("duplicate key")) {
+                        skipCount++;
+                    } else {
+                        failCount++;
+                        log.error("迁移 content_vector id={} 失败: {}", mysql.getId(), e.getMessage());
+                    }
+                }
+            }
+
+            log.info("deconstruct_content_vector → content_vectors 迁移完成,成功: {}, 跳过: {}, 失败: {}",
+                    successCount, skipCount, failCount);
+            return ReturnT.SUCCESS;
+        } catch (Exception e) {
+            log.error("content_vector 迁移失败: {}", e.getMessage(), e);
+            return new ReturnT<>(ReturnT.FAIL_CODE, "迁移失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * Redis 视频向量缓存迁移到 PG video_vectors
+     * Redis 存储结构:
+     *   Key: video:vector:{configCode}:{videoId}  Value: JSON 数组字符串
+     *   Key: video:vector:{configCode}:ids        类型: Set(存储所有 videoId)
+     */
+    @XxlJob("migrateRedisVectorToPgJob")
+    public ReturnT<String> migrateRedisVectorToPgJob(String param) {
+        log.info("开始迁移 Redis 视频向量缓存到 PG video_vectors, param: {}", param);
+
+        try {
+            // 1. 获取所有启用的配置编码
+            List<String> configCodes = getAllConfigCodes();
+            if (configCodes.isEmpty()) {
+                log.warn("未获取到任何配置编码");
+                return ReturnT.SUCCESS;
+            }
+            log.info("待迁移的配置编码: {}", configCodes);
+
+            int totalSuccess = 0;
+            int totalSkip = 0;
+            int totalFail = 0;
+
+            // 2. 逐个配置迁移
+            for (String configCode : configCodes) {
+                log.info("开始迁移配置 {} 的向量数据", configCode);
+
+                // 从 Redis Set 获取所有 videoId
+                String idsKey = VectorConstants.VECTOR_KEY_PREFIX + configCode + ":ids";
+                Set<Object> idMembers = redisTemplate.opsForSet().members(idsKey);
+
+                if (idMembers == null || idMembers.isEmpty()) {
+                    log.info("配置 {} 在 Redis 中无 videoId 索引,跳过", configCode);
+                    continue;
+                }
+
+                log.info("配置 {} 共有 {} 个 videoId 待迁移", configCode, idMembers.size());
+
+                // Redis value 可能是 Integer/Long/String,统一转为 String
+                List<String> idList = new ArrayList<>();
+                for (Object member : idMembers) {
+                    idList.add(String.valueOf(member));
+                }
+
+                // 分批处理
+                for (int i = 0; i < idList.size(); i += BATCH_SIZE) {
+                    int end = Math.min(i + BATCH_SIZE, idList.size());
+                    List<String> batch = idList.subList(i, end);
+
+                    for (String idStr : batch) {
+                        try {
+                            Long videoId = Long.parseLong(idStr);
+                            String vectorKey = VectorConstants.VECTOR_KEY_PREFIX + configCode + ":" + videoId;
+                            String vectorJson = stringRedisTemplate.opsForValue().get(vectorKey);
+
+                            if (!StringUtils.hasText(vectorJson)) {
+                                totalSkip++;
+                                continue;
+                            }
+
+                            // Redis 中存储的是归一化后的 JSON 数组 "[0.1,0.2,...]"
+                            // pgvector 接受此格式
+                            pgVideoVectorMapper.upsertVector(videoId, configCode, vectorJson);
+                            totalSuccess++;
+                        } catch (NumberFormatException e) {
+                            log.warn("非法 videoId: {}", idStr);
+                            totalSkip++;
+                        } catch (Exception e) {
+                            if (e.getMessage() != null && e.getMessage().contains("duplicate key")) {
+                                totalSkip++;
+                            } else {
+                                totalFail++;
+                                log.error("迁移 Redis 向量失败,configCode={}, videoId={}, error={}",
+                                        configCode, idStr, e.getMessage());
+                            }
+                        }
+                    }
+
+                    log.info("配置 {} 进度: {}/{}", configCode, Math.min(end, idList.size()), idList.size());
+                }
+            }
+
+            log.info("Redis 视频向量迁移完成,总成功: {}, 总跳过: {}, 总失败: {}",
+                    totalSuccess, totalSkip, totalFail);
+            return ReturnT.SUCCESS;
+        } catch (Exception e) {
+            log.error("Redis 向量迁移失败: {}", e.getMessage(), e);
+            return new ReturnT<>(ReturnT.FAIL_CODE, "迁移失败: " + e.getMessage());
+        }
+    }
+
+    // ==================== 工具方法 ====================
+
+    /**
+     * 获取所有非多点模式的配置编码(从 PG 查询 + 默认编码)
+     * 多点模式(extract_rule 非空)的数据不迁移,由 Job 重新生成
+     */
+    private List<String> getAllConfigCodes() {
+        Set<String> codes = new LinkedHashSet<>();
+        codes.add(VectorConstants.DEFAULT_CONFIG_CODE);
+
+        try {
+            DeconstructVectorConfigExample example = new DeconstructVectorConfigExample();
+            example.createCriteria().andEnabledEqualTo((short) 1);
+            List<DeconstructVectorConfig> configs = pgConfigMapper.selectByExample(example);
+            if (configs != null) {
+                for (DeconstructVectorConfig config : configs) {
+                    if (StringUtils.hasText(config.getConfigCode())
+                            && !StringUtils.hasText(config.getExtractRule())) {
+                        // 仅迁移非多点模式(extract_rule 为空)的配置
+                        codes.add(config.getConfigCode());
+                    } else if (StringUtils.hasText(config.getExtractRule())) {
+                        log.info("跳过多点模式配置: {}(extract_rule 非空,由 Job 重新生成)", config.getConfigCode());
+                    }
+                }
+            }
+        } catch (Exception e) {
+            log.error("获取配置编码失败: {}", e.getMessage());
+        }
+
+        return new ArrayList<>(codes);
+    }
+
+    /**
+     * MySQL DeconstructContent → PG DeconstructContent 实体转换
+     * 主要差异:Byte → Short
+     */
+    private DeconstructContent convertContent(
+            MysqlDeconstructContent mysql) {
+        DeconstructContent pg = new DeconstructContent();
+        // 不设置 id,让 PG 自增生成
+        pg.setTaskId(mysql.getTaskId());
+        pg.setBizType(mysql.getBizType() != null ? mysql.getBizType().shortValue() : null);
+        pg.setContentType(mysql.getContentType() != null ? mysql.getContentType().shortValue() : null);
+        pg.setChannelContentId(mysql.getChannelContentId());
+        pg.setTitle(mysql.getTitle());
+        pg.setBodyText(mysql.getBodyText());
+        pg.setVideoUrl(mysql.getVideoUrl());
+        pg.setImages(mysql.getImages());
+        pg.setChannelAccountId(mysql.getChannelAccountId());
+        pg.setChannelAccountName(mysql.getChannelAccountName());
+        pg.setStatus(mysql.getStatus() != null ? mysql.getStatus().shortValue() : null);
+        pg.setResultJson(mysql.getResultJson());
+        pg.setFailureReason(mysql.getFailureReason());
+        pg.setPointUrl(mysql.getPointUrl());
+        pg.setWeightUrl(mysql.getWeightUrl());
+        pg.setPatternUrl(mysql.getPatternUrl());
+        pg.setCreateTime(mysql.getCreateTime());
+        pg.setUpdateTime(mysql.getUpdateTime());
+        return pg;
+    }
+
+    /**
+     * MySQL DeconstructVectorConfig → PG DeconstructVectorConfig 实体转换
+     */
+    private DeconstructVectorConfig convertVectorConfig(
+            MysqlDeconstructVectorConfig mysql) {
+        DeconstructVectorConfig pg = new DeconstructVectorConfig();
+        pg.setConfigCode(mysql.getConfigCode());
+        pg.setConfigName(mysql.getConfigName());
+        pg.setBizType(mysql.getBizType() != null ? mysql.getBizType().shortValue() : null);
+        pg.setContentType(mysql.getContentType() != null ? mysql.getContentType().shortValue() : null);
+        pg.setSourceField(mysql.getSourceField());
+        pg.setSourcePath(mysql.getSourcePath());
+        pg.setExtractRule(mysql.getExtractRule());
+        pg.setEmbeddingModel(mysql.getEmbeddingModel());
+        pg.setDimension(mysql.getDimension());
+        pg.setMaxLength(mysql.getMaxLength());
+        pg.setEnableSegment(mysql.getEnableSegment() != null ? mysql.getEnableSegment().shortValue() : null);
+        pg.setSegmentSize(mysql.getSegmentSize());
+        pg.setPriority(mysql.getPriority());
+        pg.setEnabled(mysql.getEnabled() != null ? mysql.getEnabled().shortValue() : null);
+        pg.setCreateTime(mysql.getCreateTime());
+        pg.setUpdateTime(mysql.getUpdateTime());
+        return pg;
+    }
+}

+ 22 - 180
core/src/main/java/com/tzld/videoVector/job/VideoVectorJob.java

@@ -1,10 +1,12 @@
 package com.tzld.videoVector.job;
 package com.tzld.videoVector.job;
 
 
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSON;
-import com.alibaba.fastjson.JSONArray;
 import com.alibaba.fastjson.JSONObject;
 import com.alibaba.fastjson.JSONObject;
 import com.aliyun.odps.data.Record;
 import com.aliyun.odps.data.Record;
 import com.google.common.collect.Lists;
 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.dao.mapper.pgVector.DeconstructContentMapper;
 import com.tzld.videoVector.dao.mapper.pgVector.DeconstructContentMapper;
 import com.tzld.videoVector.dao.mapper.pgVector.DeconstructVectorConfigMapper;
 import com.tzld.videoVector.dao.mapper.pgVector.DeconstructVectorConfigMapper;
 import com.tzld.videoVector.model.entity.DeconstructResult;
 import com.tzld.videoVector.model.entity.DeconstructResult;
@@ -12,13 +14,11 @@ import com.tzld.videoVector.model.po.pgVector.DeconstructContent;
 import com.tzld.videoVector.model.po.pgVector.DeconstructContentExample;
 import com.tzld.videoVector.model.po.pgVector.DeconstructContentExample;
 import com.tzld.videoVector.model.po.pgVector.DeconstructVectorConfig;
 import com.tzld.videoVector.model.po.pgVector.DeconstructVectorConfig;
 import com.tzld.videoVector.model.po.pgVector.DeconstructVectorConfigExample;
 import com.tzld.videoVector.model.po.pgVector.DeconstructVectorConfigExample;
-import com.tzld.videoVector.api.AigcApiService;
-import com.tzld.videoVector.api.VideoApiService;
 import com.tzld.videoVector.service.DeconstructService;
 import com.tzld.videoVector.service.DeconstructService;
 import com.tzld.videoVector.service.EmbeddingService;
 import com.tzld.videoVector.service.EmbeddingService;
 import com.tzld.videoVector.service.VectorStoreService;
 import com.tzld.videoVector.service.VectorStoreService;
 import com.tzld.videoVector.util.OdpsUtil;
 import com.tzld.videoVector.util.OdpsUtil;
-import com.tzld.videoVector.common.constant.VectorConstants;
+import com.tzld.videoVector.util.VectorUtils;
 import com.xxl.job.core.biz.model.ReturnT;
 import com.xxl.job.core.biz.model.ReturnT;
 import com.xxl.job.core.handler.annotation.XxlJob;
 import com.xxl.job.core.handler.annotation.XxlJob;
 import lombok.extern.slf4j.Slf4j;
 import lombok.extern.slf4j.Slf4j;
@@ -79,7 +79,7 @@ public class VideoVectorJob {
 
 
             // 2. 分页处理前,每次 Job 执行只做一次审核清理
             // 2. 分页处理前,每次 Job 执行只做一次审核清理
             for (DeconstructVectorConfig config : configs) {
             for (DeconstructVectorConfig config : configs) {
-                checkAndRemoveNotAuditPassedVideos(config.getConfigCode(), isMultiPointConfig(config));
+                checkAndRemoveNotAuditPassedVideos(config.getConfigCode(), VectorUtils.isMultiPointConfig(config));
             }
             }
             log.info("审核清理完成,开始分页向量化处理");
             log.info("审核清理完成,开始分页向量化处理");
 
 
@@ -102,7 +102,7 @@ public class VideoVectorJob {
 
 
                     // 3.0 审核清理已移至分页外,此处仅进行向量存在性检查
                     // 3.0 审核清理已移至分页外,此处仅进行向量存在性检查
                     // 3.1 查询哪些 videoId 在该配置下已有向量
                     // 3.1 查询哪些 videoId 在该配置下已有向量
-                    boolean multiPoint = isMultiPointConfig(config);
+                    boolean multiPoint = VectorUtils.isMultiPointConfig(config);
                     Set<Long> existingVideoIds;
                     Set<Long> existingVideoIds;
                     if (multiPoint) {
                     if (multiPoint) {
                         // 多点模式:将 videoId 转为复合基准ID(videoId*100)检查存在性
                         // 多点模式:将 videoId 转为复合基准ID(videoId*100)检查存在性
@@ -260,7 +260,7 @@ public class VideoVectorJob {
                 texts.addAll(extractTextsWithConfidence(json, sourcePath, extractRule));
                 texts.addAll(extractTextsWithConfidence(json, sourcePath, extractRule));
             } else {
             } else {
                 // 单点模式:直接提取文本值(向后兼容)
                 // 单点模式:直接提取文本值(向后兼容)
-                texts.addAll(extractFromJson(json, sourcePath));
+                texts.addAll(VectorUtils.extractFromJson(json, sourcePath));
             }
             }
 
 
         } catch (Exception e) {
         } catch (Exception e) {
@@ -294,7 +294,7 @@ public class VideoVectorJob {
             }
             }
 
 
             // 提取数组项(sourcePath 以 [*] 结尾,提取整个对象列表)
             // 提取数组项(sourcePath 以 [*] 结尾,提取整个对象列表)
-            List<JSONObject> items = extractArrayItemsFromJson(json, sourcePath);
+            List<JSONObject> items = VectorUtils.extractArrayItemsFromJson(json, sourcePath);
 
 
             for (JSONObject item : items) {
             for (JSONObject item : items) {
                 if (isConfidenceQualified(item, confidenceField, confidenceThreshold)) {
                 if (isConfidenceQualified(item, confidenceField, confidenceThreshold)) {
@@ -314,64 +314,6 @@ public class VideoVectorJob {
         return texts;
         return texts;
     }
     }
 
 
-    /**
-     * 从JSON中提取数组项对象列表
-     * 支持路径以 [*] 结尾,如 $.a.b.c[*]
-     */
-    private List<JSONObject> extractArrayItemsFromJson(JSONObject json, String sourcePath) {
-        List<JSONObject> items = new ArrayList<>();
-
-        if (json == null || !StringUtils.hasText(sourcePath)) {
-            return items;
-        }
-
-        try {
-            if (!sourcePath.startsWith("$.")) {
-                return items;
-            }
-
-            String pathContent = sourcePath.substring(2);
-            // 路径应以 [*] 结尾
-            if (!pathContent.endsWith("[*]")) {
-                log.warn("多点模式下 source_path 应以 [*] 结尾: {}", sourcePath);
-                return items;
-            }
-
-            // 去掉末尾的 [*],找到数组所在的父路径和数组字段名
-            String pathWithoutWildcard = pathContent.substring(0, pathContent.length() - 3);
-            List<String> parts = parseJsonPath(pathWithoutWildcard);
-
-            // 逐层访问到数组字段的父对象
-            Object current = json;
-            for (int i = 0; i < parts.size() - 1; i++) {
-                if (current instanceof JSONObject) {
-                    current = ((JSONObject) current).get(parts.get(i));
-                } else {
-                    return items;
-                }
-            }
-
-            // 获取数组
-            if (current instanceof JSONObject && !parts.isEmpty()) {
-                String arrayKey = parts.get(parts.size() - 1);
-                JSONArray array = ((JSONObject) current).getJSONArray(arrayKey);
-                if (array != null) {
-                    for (int i = 0; i < array.size(); i++) {
-                        JSONObject item = array.getJSONObject(i);
-                        if (item != null) {
-                            items.add(item);
-                        }
-                    }
-                }
-            }
-
-        } catch (Exception e) {
-            log.error("提取数组项失败: path={}, error={}", sourcePath, e.getMessage());
-        }
-
-        return items;
-    }
-
     /**
     /**
      * 判断置信度是否满足条件
      * 判断置信度是否满足条件
      * 规则:置信度 == "high"(字符串)或 置信度 > threshold(数值)
      * 规则:置信度 == "high"(字符串)或 置信度 > threshold(数值)
@@ -386,18 +328,11 @@ public class VideoVectorJob {
             return "high".equalsIgnoreCase((String) value);
             return "high".equalsIgnoreCase((String) value);
         }
         }
         if (value instanceof Number) {
         if (value instanceof Number) {
-            return ((Number) value).doubleValue() > threshold;
+            return ((Number) value).doubleValue() >= threshold;
         }
         }
         return false;
         return false;
     }
     }
 
 
-    /**
-     * 判断配置是否为多点模式(有 extract_rule 配置)
-     */
-    private static boolean isMultiPointConfig(DeconstructVectorConfig config) {
-        return StringUtils.hasText(config.getExtractRule());
-    }
-
     /**
     /**
      * 编码多点复合ID
      * 编码多点复合ID
      * @param videoId 原始视频ID
      * @param videoId 原始视频ID
@@ -405,7 +340,7 @@ public class VideoVectorJob {
      * @return 复合ID = videoId * 100 + index
      * @return 复合ID = videoId * 100 + index
      */
      */
     private static Long encodeMultiPointId(Long videoId, int index) {
     private static Long encodeMultiPointId(Long videoId, int index) {
-        return videoId * VectorConstants.MULTI_POINT_FACTOR + index;
+        return VectorUtils.encodeMultiPointId(videoId, index);
     }
     }
 
 
     /**
     /**
@@ -414,106 +349,7 @@ public class VideoVectorJob {
      * @return 原始视频ID = compositeId / 100
      * @return 原始视频ID = compositeId / 100
      */
      */
     private static Long decodeVideoId(Long compositeId) {
     private static Long decodeVideoId(Long compositeId) {
-        return compositeId / VectorConstants.MULTI_POINT_FACTOR;
-    }
-
-    /**
-     * 从JSON中提取文本
-     * 支持路径格式:$.final_normalization_rebuild.topic_fusion_result.最终选题.选题
-     */
-    private List<String> extractFromJson(JSONObject json, String path) {
-        List<String> results = new ArrayList<>();
-        
-        if (json == null || !StringUtils.hasText(path)) {
-            return results;
-        }
-        
-        try {
-            // 路径处理
-            if (path.startsWith("$.")) {
-                String pathContent = path.substring(2);
-                List<String> parts = parseJsonPath(pathContent);
-                Object current = json;
-
-                for (int i = 0; i < parts.size(); i++) {
-                    String part = parts.get(i);
-                    if (current == null) {
-                        break;
-                    }
-
-                    // 处理数组路径(如 topics[*])
-                    if (part.endsWith("[*]")) {
-                        String arrayKey = part.substring(0, part.length() - 3);
-                        if (current instanceof JSONObject) {
-                            JSONArray array = ((JSONObject) current).getJSONArray(arrayKey);
-                            if (array != null) {
-                                List<String> remainingParts = parts.subList(i + 1, parts.size());
-                                String remainingPath = String.join(".", remainingParts);
-                                for (int j = 0; j < array.size(); j++) {
-                                    Object item = array.get(j);
-                                    if (remainingParts.isEmpty()) {
-                                        if (item instanceof String) {
-                                            results.add((String) item);
-                                        }
-                                    } else {
-                                        results.addAll(extractFromJson(
-                                                JSON.parseObject(JSON.toJSONString(item)), "$." + remainingPath));
-                                    }
-                                }
-                            }
-                        }
-                        return results;
-                    } else {
-                        if (current instanceof JSONObject) {
-                            current = ((JSONObject) current).get(part);
-                        } else {
-                            break;
-                        }
-                    }
-                }
-
-                if (current instanceof String) {
-                    results.add((String) current);
-                } else if (current instanceof JSONArray) {
-                    JSONArray array = (JSONArray) current;
-                    for (int i = 0; i < array.size(); i++) {
-                        Object item = array.get(i);
-                        if (item instanceof String) {
-                            results.add((String) item);
-                        }
-                    }
-                }
-            }
-        } catch (Exception e) {
-            log.error("JSON提取失败,path={}, error={}", path, e.getMessage());
-        }
-
-        return results;
-    }
-
-    /**
-     * 解析 JSONPath 路径
-     */
-    private List<String> parseJsonPath(String pathContent) {
-        List<String> parts = new ArrayList<>();
-        StringBuilder current = new StringBuilder();
-        
-        for (int i = 0; i < pathContent.length(); i++) {
-            char c = pathContent.charAt(i);
-            if (c == '.') {
-                if (current.length() > 0) {
-                    parts.add(current.toString());
-                    current = new StringBuilder();
-                }
-            } else {
-                current.append(c);
-            }
-        }
-        if (current.length() > 0) {
-            parts.add(current.toString());
-        }
-        
-        return parts;
+        return VectorUtils.decodeVideoId(compositeId);
     }
     }
 
 
     /**
     /**
@@ -531,12 +367,18 @@ public class VideoVectorJob {
     private int vectorizeAndStore(DeconstructVectorConfig config, Long videoId, List<String> texts) {
     private int vectorizeAndStore(DeconstructVectorConfig config, Long videoId, List<String> texts) {
         String configCode = config.getConfigCode();
         String configCode = config.getConfigCode();
         Integer maxLength = config.getMaxLength();
         Integer maxLength = config.getMaxLength();
-        boolean multiPoint = isMultiPointConfig(config);
+        boolean multiPoint = VectorUtils.isMultiPointConfig(config);
 
 
         if (multiPoint) {
         if (multiPoint) {
             // ---- 多点模式:每个文本独立向量化存储 ----
             // ---- 多点模式:每个文本独立向量化存储 ----
+            int maxPoints = (int) VectorConstants.MULTI_POINT_FACTOR;
+            int limit = Math.min(texts.size(), maxPoints);
+            if (texts.size() > maxPoints) {
+                log.warn("videoId={} 配置 {} 文本数量 {} 超过多点模式上限 {},仅处理前 {} 个",
+                        videoId, configCode, texts.size(), maxPoints, maxPoints);
+            }
             int successCount = 0;
             int successCount = 0;
-            for (int i = 0; i < texts.size(); i++) {
+            for (int i = 0; i < limit; i++) {
                 String text = texts.get(i);
                 String text = texts.get(i);
                 if (!StringUtils.hasText(text)) {
                 if (!StringUtils.hasText(text)) {
                     continue;
                     continue;
@@ -663,7 +505,7 @@ public class VideoVectorJob {
                 String configCode = config.getConfigCode();
                 String configCode = config.getConfigCode();
 
 
                 // 4.1 查询该配置下已有向量的 videoId,排除已处理过的
                 // 4.1 查询该配置下已有向量的 videoId,排除已处理过的
-                boolean multiPoint = isMultiPointConfig(config);
+                boolean multiPoint = VectorUtils.isMultiPointConfig(config);
                 Set<Long> existingVideoIds;
                 Set<Long> existingVideoIds;
                 if (multiPoint) {
                 if (multiPoint) {
                     List<Long> baseIds = allVideoIds.stream()
                     List<Long> baseIds = allVideoIds.stream()
@@ -761,7 +603,7 @@ public class VideoVectorJob {
             return extractTextsWithConfidence(dataContent, config.getSourcePath(), extractRule);
             return extractTextsWithConfidence(dataContent, config.getSourcePath(), extractRule);
         } else {
         } else {
             // 单点模式:直接提取
             // 单点模式:直接提取
-            return extractFromJson(dataContent, config.getSourcePath());
+            return VectorUtils.extractFromJson(dataContent, config.getSourcePath());
         }
         }
     }
     }
 
 
@@ -775,7 +617,7 @@ public class VideoVectorJob {
             return Collections.emptyList();
             return Collections.emptyList();
         }
         }
         String sourcePath = "$.最终选题.选题";
         String sourcePath = "$.最终选题.选题";
-        return extractFromJson(dataContent, sourcePath);
+        return VectorUtils.extractFromJson(dataContent, sourcePath);
     }
     }
 
 
     /**
     /**

+ 1 - 1
core/src/main/java/com/tzld/videoVector/model/po/videoVector/deconstruct/DeconstructContent.java → core/src/main/java/com/tzld/videoVector/model/po/videoVector/deconstruct/MysqlDeconstructContent.java

@@ -7,7 +7,7 @@ import java.util.Date;
  * This class was generated by MyBatis Generator.
  * This class was generated by MyBatis Generator.
  * This class corresponds to the database table deconstruct_content
  * This class corresponds to the database table deconstruct_content
  */
  */
-public class DeconstructContent {
+public class MysqlDeconstructContent {
     /**
     /**
      * Database Column Remarks:
      * Database Column Remarks:
      *   主键ID
      *   主键ID

+ 2 - 2
core/src/main/java/com/tzld/videoVector/model/po/videoVector/deconstruct/DeconstructContentExample.java → core/src/main/java/com/tzld/videoVector/model/po/videoVector/deconstruct/MysqlDeconstructContentExample.java

@@ -4,7 +4,7 @@ import java.util.ArrayList;
 import java.util.Date;
 import java.util.Date;
 import java.util.List;
 import java.util.List;
 
 
-public class DeconstructContentExample {
+public class MysqlDeconstructContentExample {
     /**
     /**
      * This field was generated by MyBatis Generator.
      * This field was generated by MyBatis Generator.
      * This field corresponds to the database table deconstruct_content
      * This field corresponds to the database table deconstruct_content
@@ -35,7 +35,7 @@ public class DeconstructContentExample {
      *
      *
      * @mbg.generated Mon Mar 09 10:39:51 CST 2026
      * @mbg.generated Mon Mar 09 10:39:51 CST 2026
      */
      */
-    public DeconstructContentExample() {
+    public MysqlDeconstructContentExample() {
         oredCriteria = new ArrayList<Criteria>();
         oredCriteria = new ArrayList<Criteria>();
     }
     }
 
 

+ 1 - 1
core/src/main/java/com/tzld/videoVector/model/po/videoVector/deconstruct/DeconstructContentVector.java → core/src/main/java/com/tzld/videoVector/model/po/videoVector/deconstruct/MysqlDeconstructContentVector.java

@@ -7,7 +7,7 @@ import java.util.Date;
  * This class was generated by MyBatis Generator.
  * This class was generated by MyBatis Generator.
  * This class corresponds to the database table deconstruct_content_vector
  * This class corresponds to the database table deconstruct_content_vector
  */
  */
-public class DeconstructContentVector {
+public class MysqlDeconstructContentVector {
     /**
     /**
      * Database Column Remarks:
      * Database Column Remarks:
      *   主键ID
      *   主键ID

+ 2 - 2
core/src/main/java/com/tzld/videoVector/model/po/videoVector/deconstruct/DeconstructContentVectorExample.java → core/src/main/java/com/tzld/videoVector/model/po/videoVector/deconstruct/MysqlDeconstructContentVectorExample.java

@@ -4,7 +4,7 @@ import java.util.ArrayList;
 import java.util.Date;
 import java.util.Date;
 import java.util.List;
 import java.util.List;
 
 
-public class DeconstructContentVectorExample {
+public class MysqlDeconstructContentVectorExample {
     /**
     /**
      * This field was generated by MyBatis Generator.
      * This field was generated by MyBatis Generator.
      * This field corresponds to the database table deconstruct_content_vector
      * This field corresponds to the database table deconstruct_content_vector
@@ -35,7 +35,7 @@ public class DeconstructContentVectorExample {
      *
      *
      * @mbg.generated Thu Apr 23 15:03:47 CST 2026
      * @mbg.generated Thu Apr 23 15:03:47 CST 2026
      */
      */
-    public DeconstructContentVectorExample() {
+    public MysqlDeconstructContentVectorExample() {
         oredCriteria = new ArrayList<Criteria>();
         oredCriteria = new ArrayList<Criteria>();
     }
     }
 
 

+ 1 - 1
core/src/main/java/com/tzld/videoVector/model/po/videoVector/deconstruct/DeconstructVectorConfig.java → core/src/main/java/com/tzld/videoVector/model/po/videoVector/deconstruct/MysqlDeconstructVectorConfig.java

@@ -7,7 +7,7 @@ import java.util.Date;
  * This class was generated by MyBatis Generator.
  * This class was generated by MyBatis Generator.
  * This class corresponds to the database table deconstruct_vector_config
  * This class corresponds to the database table deconstruct_vector_config
  */
  */
-public class DeconstructVectorConfig {
+public class MysqlDeconstructVectorConfig {
     /**
     /**
      * Database Column Remarks:
      * Database Column Remarks:
      *   主键ID
      *   主键ID

+ 2 - 2
core/src/main/java/com/tzld/videoVector/model/po/videoVector/deconstruct/DeconstructVectorConfigExample.java → core/src/main/java/com/tzld/videoVector/model/po/videoVector/deconstruct/MysqlDeconstructVectorConfigExample.java

@@ -4,7 +4,7 @@ import java.util.ArrayList;
 import java.util.Date;
 import java.util.Date;
 import java.util.List;
 import java.util.List;
 
 
-public class DeconstructVectorConfigExample {
+public class MysqlDeconstructVectorConfigExample {
     /**
     /**
      * This field was generated by MyBatis Generator.
      * This field was generated by MyBatis Generator.
      * This field corresponds to the database table deconstruct_vector_config
      * This field corresponds to the database table deconstruct_vector_config
@@ -35,7 +35,7 @@ public class DeconstructVectorConfigExample {
      *
      *
      * @mbg.generated Thu Apr 23 14:55:06 CST 2026
      * @mbg.generated Thu Apr 23 14:55:06 CST 2026
      */
      */
-    public DeconstructVectorConfigExample() {
+    public MysqlDeconstructVectorConfigExample() {
         oredCriteria = new ArrayList<Criteria>();
         oredCriteria = new ArrayList<Criteria>();
     }
     }
 
 

+ 29 - 0
core/src/main/java/com/tzld/videoVector/model/vo/VideoMatchResult.java

@@ -0,0 +1,29 @@
+package com.tzld.videoVector.model.vo;
+
+import lombok.Data;
+
+/**
+ * 视频匹配结果包装类
+ * 替代原来手动拼接 JSONObject 的返回方式,提供类型安全
+ */
+@Data
+public class VideoMatchResult {
+
+    /** 命中的配置编码(如 VIDEO_TOPIC、VIDEO_KEYPOINT 等) */
+    private String configCode;
+
+    /** 匹配到的视频ID */
+    private Long videoId;
+
+    /** 余弦相似度分值 */
+    private Double score;
+
+    public VideoMatchResult() {
+    }
+
+    public VideoMatchResult(String configCode, Long videoId, Double score) {
+        this.configCode = configCode;
+        this.videoId = videoId;
+        this.score = score;
+    }
+}

+ 7 - 7
core/src/main/java/com/tzld/videoVector/service/VectorizeService.java

@@ -1,7 +1,7 @@
 package com.tzld.videoVector.service;
 package com.tzld.videoVector.service;
 
 
+import com.tzld.videoVector.model.po.pgVector.ContentVector;
 import com.tzld.videoVector.model.po.pgVector.DeconstructContent;
 import com.tzld.videoVector.model.po.pgVector.DeconstructContent;
-import com.tzld.videoVector.model.po.videoVector.deconstruct.DeconstructContentVector;
 import com.tzld.videoVector.model.po.pgVector.DeconstructVectorConfig;
 import com.tzld.videoVector.model.po.pgVector.DeconstructVectorConfig;
 
 
 import java.util.List;
 import java.util.List;
@@ -27,7 +27,7 @@ public interface VectorizeService {
      * @param content 解构内容
      * @param content 解构内容
      * @return 生成的向量列表
      * @return 生成的向量列表
      */
      */
-    List<DeconstructContentVector> vectorizeContent(DeconstructContent content);
+    List<ContentVector> vectorizeContent(DeconstructContent content);
 
 
     /**
     /**
      * 根据配置提取文本并进行向量化
      * 根据配置提取文本并进行向量化
@@ -36,7 +36,7 @@ public interface VectorizeService {
      * @param config  向量化配置
      * @param config  向量化配置
      * @return 向量列表(支持分段)
      * @return 向量列表(支持分段)
      */
      */
-    List<DeconstructContentVector> vectorizeByConfig(DeconstructContent content, DeconstructVectorConfig config);
+    List<ContentVector> vectorizeByConfig(DeconstructContent content, DeconstructVectorConfig config);
 
 
     /**
     /**
      * 批量保存向量数据
      * 批量保存向量数据
@@ -44,7 +44,7 @@ public interface VectorizeService {
      * @param vectors 向量列表
      * @param vectors 向量列表
      * @return 保存数量
      * @return 保存数量
      */
      */
-    int batchSaveVectors(List<DeconstructContentVector> vectors);
+    int batchSaveVectors(List<ContentVector> vectors);
 
 
     /**
     /**
      * 根据内容ID查询向量列表
      * 根据内容ID查询向量列表
@@ -52,7 +52,7 @@ public interface VectorizeService {
      * @param contentId 内容ID
      * @param contentId 内容ID
      * @return 向量列表
      * @return 向量列表
      */
      */
-    List<DeconstructContentVector> getVectorsByContentId(Long contentId);
+    List<ContentVector> getVectorsByContentId(Long contentId);
 
 
     /**
     /**
      * 根据内容ID和配置编码查询向量列表
      * 根据内容ID和配置编码查询向量列表
@@ -61,7 +61,7 @@ public interface VectorizeService {
      * @param configCode 配置编码,为空时不过滤
      * @param configCode 配置编码,为空时不过滤
      * @return 向量列表
      * @return 向量列表
      */
      */
-    List<DeconstructContentVector> getVectorsByContentId(Long contentId, String configCode);
+    List<ContentVector> getVectorsByContentId(Long contentId, String configCode);
 
 
     /**
     /**
      * 根据内容ID和字段标识查询向量
      * 根据内容ID和字段标识查询向量
@@ -70,5 +70,5 @@ public interface VectorizeService {
      * @param sourceField 来源字段
      * @param sourceField 来源字段
      * @return 向量列表
      * @return 向量列表
      */
      */
-    List<DeconstructContentVector> getVectorsByField(Long contentId, String sourceField);
+    List<ContentVector> getVectorsByField(Long contentId, String sourceField);
 }
 }

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

@@ -5,6 +5,8 @@ import com.tzld.videoVector.model.param.DeconstructParam;
 import com.tzld.videoVector.model.param.GetDeconstructParam;
 import com.tzld.videoVector.model.param.GetDeconstructParam;
 import com.tzld.videoVector.model.param.MatchTopNVideoParam;
 import com.tzld.videoVector.model.param.MatchTopNVideoParam;
 
 
+import com.tzld.videoVector.model.vo.VideoMatchResult;
+
 import java.util.List;
 import java.util.List;
 
 
 public interface VideoSearchService {
 public interface VideoSearchService {
@@ -28,5 +30,5 @@ public interface VideoSearchService {
      * @param param 匹配参数
      * @param param 匹配参数
      * @return 匹配结果列表
      * @return 匹配结果列表
      */
      */
-    List<Object> matchTopNVideo(MatchTopNVideoParam param);
+    List<VideoMatchResult> matchTopNVideo(MatchTopNVideoParam param);
 }
 }

+ 3 - 29
core/src/main/java/com/tzld/videoVector/service/impl/PgVectorStoreServiceImpl.java

@@ -4,6 +4,7 @@ import com.tzld.videoVector.dao.mapper.pgVector.VideoVectorMapper;
 import com.tzld.videoVector.model.entity.VideoMatch;
 import com.tzld.videoVector.model.entity.VideoMatch;
 import com.tzld.videoVector.model.po.pgVector.VideoVector;
 import com.tzld.videoVector.model.po.pgVector.VideoVector;
 import com.tzld.videoVector.service.VectorStoreService;
 import com.tzld.videoVector.service.VectorStoreService;
+import com.tzld.videoVector.util.VectorUtils;
 import lombok.extern.slf4j.Slf4j;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.stereotype.Service;
@@ -107,7 +108,7 @@ public class PgVectorStoreServiceImpl implements VectorStoreService {
         }
         }
 
 
         String embedding = videoVectorMapper.selectEmbeddingByVideoIdAndConfigCode(videoId, configCode);
         String embedding = videoVectorMapper.selectEmbeddingByVideoIdAndConfigCode(videoId, configCode);
-        return parseVectorString(embedding);
+        return VectorUtils.parseVectorString(embedding);
     }
     }
 
 
     @Override
     @Override
@@ -132,7 +133,7 @@ public class PgVectorStoreServiceImpl implements VectorStoreService {
             List<VideoVector> vectors = videoVectorMapper.selectVectorsByVideoIds(batch, configCode);
             List<VideoVector> vectors = videoVectorMapper.selectVectorsByVideoIds(batch, configCode);
             if (vectors != null) {
             if (vectors != null) {
                 for (VideoVector vv : vectors) {
                 for (VideoVector vv : vectors) {
-                    List<Float> parsed = parseVectorString(vv.getEmbedding());
+                    List<Float> parsed = VectorUtils.parseVectorString(vv.getEmbedding());
                     if (parsed != null) {
                     if (parsed != null) {
                         result.put(vv.getVideoId(), parsed);
                         result.put(vv.getVideoId(), parsed);
                     }
                     }
@@ -241,31 +242,4 @@ public class PgVectorStoreServiceImpl implements VectorStoreService {
         sb.append("]");
         sb.append("]");
         return sb.toString();
         return sb.toString();
     }
     }
-
-    /**
-     * 解析 pgvector 字符串为 List<Float>
-     * 格式: "[0.1,0.2,...]"
-     */
-    private List<Float> parseVectorString(String vectorStr) {
-        if (vectorStr == null || vectorStr.isEmpty()) return null;
-        try {
-            // 去除首尾的方括号
-            String trimmed = vectorStr.trim();
-            if (trimmed.startsWith("[")) {
-                trimmed = trimmed.substring(1);
-            }
-            if (trimmed.endsWith("]")) {
-                trimmed = trimmed.substring(0, trimmed.length() - 1);
-            }
-            String[] parts = trimmed.split(",");
-            List<Float> result = new ArrayList<>(parts.length);
-            for (String part : parts) {
-                result.add(Float.parseFloat(part.trim()));
-            }
-            return result;
-        } catch (Exception e) {
-            log.error("向量字符串解析失败: {}", e.getMessage());
-            return null;
-        }
-    }
 }
 }

+ 26 - 206
core/src/main/java/com/tzld/videoVector/service/impl/VectorizeServiceImpl.java

@@ -1,17 +1,15 @@
 package com.tzld.videoVector.service.impl;
 package com.tzld.videoVector.service.impl;
 
 
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSON;
-import com.alibaba.fastjson.JSONArray;
-import com.alibaba.fastjson.JSONObject;
 import com.tzld.videoVector.dao.mapper.pgVector.ContentVectorMapper;
 import com.tzld.videoVector.dao.mapper.pgVector.ContentVectorMapper;
 import com.tzld.videoVector.dao.mapper.pgVector.DeconstructVectorConfigMapper;
 import com.tzld.videoVector.dao.mapper.pgVector.DeconstructVectorConfigMapper;
 import com.tzld.videoVector.model.po.pgVector.ContentVector;
 import com.tzld.videoVector.model.po.pgVector.ContentVector;
 import com.tzld.videoVector.model.po.pgVector.DeconstructContent;
 import com.tzld.videoVector.model.po.pgVector.DeconstructContent;
 import com.tzld.videoVector.model.po.pgVector.DeconstructVectorConfig;
 import com.tzld.videoVector.model.po.pgVector.DeconstructVectorConfig;
-import com.tzld.videoVector.model.po.videoVector.deconstruct.DeconstructContentVector;
 import com.tzld.videoVector.service.EmbeddingService;
 import com.tzld.videoVector.service.EmbeddingService;
 import com.tzld.videoVector.service.VectorizeService;
 import com.tzld.videoVector.service.VectorizeService;
 import com.tzld.videoVector.util.Md5Util;
 import com.tzld.videoVector.util.Md5Util;
+import com.tzld.videoVector.util.VectorUtils;
 import lombok.extern.slf4j.Slf4j;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 import org.springframework.stereotype.Service;
 import org.springframework.util.CollectionUtils;
 import org.springframework.util.CollectionUtils;
@@ -21,7 +19,6 @@ import javax.annotation.Resource;
 import java.util.ArrayList;
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.Date;
 import java.util.List;
 import java.util.List;
-import java.util.stream.Collectors;
 
 
 /**
 /**
  * 向量化服务实现类
  * 向量化服务实现类
@@ -49,7 +46,7 @@ public class VectorizeServiceImpl implements VectorizeService {
     }
     }
 
 
     @Override
     @Override
-    public List<DeconstructContentVector> vectorizeContent(DeconstructContent content) {
+    public List<ContentVector> vectorizeContent(DeconstructContent content) {
         if (content == null || content.getId() == null) {
         if (content == null || content.getId() == null) {
             log.error("向量化内容为空或ID为空");
             log.error("向量化内容为空或ID为空");
             return new ArrayList<>();
             return new ArrayList<>();
@@ -67,19 +64,19 @@ public class VectorizeServiceImpl implements VectorizeService {
             return new ArrayList<>();
             return new ArrayList<>();
         }
         }
 
 
-        List<DeconstructContentVector> allVectors = new ArrayList<>();
+        List<ContentVector> allVectors = new ArrayList<>();
 
 
         // 按配置逐个向量化
         // 按配置逐个向量化
         for (DeconstructVectorConfig config : configs) {
         for (DeconstructVectorConfig config : configs) {
             try {
             try {
                 // 幂等检查:若该 configCode 已有向量则跳过,防止重复向量化
                 // 幂等检查:若该 configCode 已有向量则跳过,防止重复向量化
-                List<DeconstructContentVector> existing = getVectorsByContentId(content.getId(), config.getConfigCode());
+                List<ContentVector> existing = getVectorsByContentId(content.getId(), config.getConfigCode());
                 if (!CollectionUtils.isEmpty(existing)) {
                 if (!CollectionUtils.isEmpty(existing)) {
                     log.debug("contentId={} 已有 configCode={} 的向量,跳过向量化",
                     log.debug("contentId={} 已有 configCode={} 的向量,跳过向量化",
                             content.getId(), config.getConfigCode());
                             content.getId(), config.getConfigCode());
                     continue;
                     continue;
                 }
                 }
-                List<DeconstructContentVector> vectors = vectorizeByConfig(content, config);
+                List<ContentVector> vectors = vectorizeByConfig(content, config);
                 if (!CollectionUtils.isEmpty(vectors)) {
                 if (!CollectionUtils.isEmpty(vectors)) {
                     allVectors.addAll(vectors);
                     allVectors.addAll(vectors);
                 }
                 }
@@ -100,9 +97,9 @@ public class VectorizeServiceImpl implements VectorizeService {
     }
     }
 
 
     @Override
     @Override
-    public List<DeconstructContentVector> vectorizeByConfig(DeconstructContent content,
-                                                            DeconstructVectorConfig config) {
-        List<DeconstructContentVector> vectors = new ArrayList<>();
+    public List<ContentVector> vectorizeByConfig(DeconstructContent content,
+                                                                 DeconstructVectorConfig config) {
+        List<ContentVector> vectors = new ArrayList<>();
 
 
         // 提取文本内容
         // 提取文本内容
         List<String> texts = extractTexts(content, config);
         List<String> texts = extractTexts(content, config);
@@ -137,22 +134,21 @@ public class VectorizeServiceImpl implements VectorizeService {
                 }
                 }
             }
             }
 
 
-            // 构建向量实体
-            DeconstructContentVector vector = new DeconstructContentVector();
+            // 构建向量实体(直接使用 PG ContentVector)
+            ContentVector vector = new ContentVector();
             vector.setContentId(content.getId());
             vector.setContentId(content.getId());
             vector.setTaskId(content.getTaskId());
             vector.setTaskId(content.getTaskId());
             vector.setConfigCode(config.getConfigCode());
             vector.setConfigCode(config.getConfigCode());
             vector.setSourceField(config.getSourceField());
             vector.setSourceField(config.getSourceField());
             vector.setSourcePath(config.getSourcePath());
             vector.setSourcePath(config.getSourcePath());
-            vector.setVectorDimension(vectorData.size());
-            vector.setVectorData(JSON.toJSONString(vectorData));
+            vector.setEmbedding(JSON.toJSONString(vectorData));
             vector.setSourceText(truncatedText);
             vector.setSourceText(truncatedText);
             vector.setTextHash(textHash);
             vector.setTextHash(textHash);
             vector.setEmbeddingModel(embeddingModel);
             vector.setEmbeddingModel(embeddingModel);
             vector.setSegmentIndex(segmentIndex++);
             vector.setSegmentIndex(segmentIndex++);
             vector.setSegmentTotal(texts.size());
             vector.setSegmentTotal(texts.size());
-            vector.setCreateTime(new Date());
-            vector.setUpdateTime(new Date());
+            vector.setCreatedAt(new Date());
+            vector.setUpdatedAt(new Date());
 
 
             vectors.add(vector);
             vectors.add(vector);
         }
         }
@@ -183,13 +179,13 @@ public class VectorizeServiceImpl implements VectorizeService {
             case "result_json":
             case "result_json":
                 // 从解构结果JSON中提取
                 // 从解构结果JSON中提取
                 if (StringUtils.hasText(content.getResultJson()) && StringUtils.hasText(sourcePath)) {
                 if (StringUtils.hasText(content.getResultJson()) && StringUtils.hasText(sourcePath)) {
-                    texts.addAll(extractFromJson(content.getResultJson(), sourcePath));
+                    texts.addAll(VectorUtils.extractFromJson(content.getResultJson(), sourcePath));
                 }
                 }
                 break;
                 break;
             default:
             default:
                 // 尝试从result_json中提取自定义字段
                 // 尝试从result_json中提取自定义字段
                 if (StringUtils.hasText(content.getResultJson()) && StringUtils.hasText(sourcePath)) {
                 if (StringUtils.hasText(content.getResultJson()) && StringUtils.hasText(sourcePath)) {
-                    texts.addAll(extractFromJson(content.getResultJson(), sourcePath));
+                    texts.addAll(VectorUtils.extractFromJson(content.getResultJson(), sourcePath));
                 }
                 }
                 break;
                 break;
         }
         }
@@ -197,120 +193,6 @@ public class VectorizeServiceImpl implements VectorizeService {
         return texts;
         return texts;
     }
     }
 
 
-    /**
-     * 从JSON中提取文本
-     * 支持路径格式:$.final_normalization_rebuild.topic_fusion_result.最终选题.选题
-     * 支持中文 key 和数组路径如 topics[*]
-     */
-    private List<String> extractFromJson(String json, String path) {
-        List<String> results = new ArrayList<>();
-
-        if (!StringUtils.hasText(json) || !StringUtils.hasText(path)) {
-            return results;
-        }
-
-        try {
-            JSONObject jsonObject = JSON.parseObject(json);
-            if (jsonObject == null) {
-                return results;
-            }
-
-            // 路径处理(如 $.final_normalization_rebuild.topic_fusion_result.最终选题.选题)
-            if (path.startsWith("$.")) {
-                String pathContent = path.substring(2);
-                // 使用正则分割,支持中文 key
-                List<String> parts = parseJsonPath(pathContent);
-                Object current = jsonObject;
-
-                for (int i = 0; i < parts.size(); i++) {
-                    String part = parts.get(i);
-                    if (current == null) {
-                        break;
-                    }
-
-                    // 处理数组路径(如 topics[*])
-                    if (part.endsWith("[*]")) {
-                        String arrayKey = part.substring(0, part.length() - 3);
-                        if (current instanceof JSONObject) {
-                            JSONArray array = ((JSONObject) current).getJSONArray(arrayKey);
-                            if (array != null) {
-                                // 递归处理数组元素
-                                List<String> remainingParts = parts.subList(i + 1, parts.size());
-                                String remainingPath = String.join(".", remainingParts);
-                                for (int j = 0; j < array.size(); j++) {
-                                    Object item = array.get(j);
-                                    if (remainingParts.isEmpty()) {
-                                        // 直接取数组元素值
-                                        if (item instanceof String) {
-                                            results.add((String) item);
-                                        }
-                                    } else {
-                                        // 继续深入
-                                        results.addAll(extractFromJson(
-                                                JSON.toJSONString(item), "$." + remainingPath));
-                                    }
-                                }
-                            }
-                        }
-                        return results;
-                    } else {
-                        // 普通对象路径(支持中文 key)
-                        if (current instanceof JSONObject) {
-                            current = ((JSONObject) current).get(part);
-                        } else {
-                            // 当前节点不是对象,无法继续
-                            break;
-                        }
-                    }
-                }
-
-                // 提取最终值
-                if (current instanceof String) {
-                    results.add((String) current);
-                } else if (current instanceof JSONArray) {
-                    JSONArray array = (JSONArray) current;
-                    for (int i = 0; i < array.size(); i++) {
-                        Object item = array.get(i);
-                        if (item instanceof String) {
-                            results.add((String) item);
-                        }
-                    }
-                }
-            }
-        } catch (Exception e) {
-            log.error("JSON提取失败,path={}, error={}", path, e.getMessage());
-        }
-
-        return results;
-    }
-
-    /**
-     * 解析 JSONPath 路径,支持中文 key
-     * 例如:final_normalization_rebuild.topic_fusion_result.最终选题.选题
-     */
-    private List<String> parseJsonPath(String pathContent) {
-        List<String> parts = new ArrayList<>();
-        StringBuilder current = new StringBuilder();
-        
-        for (int i = 0; i < pathContent.length(); i++) {
-            char c = pathContent.charAt(i);
-            if (c == '.') {
-                if (current.length() > 0) {
-                    parts.add(current.toString());
-                    current = new StringBuilder();
-                }
-            } else {
-                current.append(c);
-            }
-        }
-        // 添加最后一个部分
-        if (current.length() > 0) {
-            parts.add(current.toString());
-        }
-        
-        return parts;
-    }
-
     /**
     /**
      * 文本截断
      * 文本截断
      * maxLength 为 null 或 <= 0 时不进行截断
      * maxLength 为 null 或 <= 0 时不进行截断
@@ -341,7 +223,7 @@ public class VectorizeServiceImpl implements VectorizeService {
             // 从 pgvector 查询
             // 从 pgvector 查询
             ContentVector cached = pgContentVectorMapper.selectByTextHashAndConfigCode(textHash, configCode);
             ContentVector cached = pgContentVectorMapper.selectByTextHashAndConfigCode(textHash, configCode);
             if (cached != null && StringUtils.hasText(cached.getEmbedding())) {
             if (cached != null && StringUtils.hasText(cached.getEmbedding())) {
-                return parseVectorString(cached.getEmbedding());
+                return VectorUtils.parseVectorString(cached.getEmbedding());
             }
             }
         } catch (Exception e) {
         } catch (Exception e) {
             log.error("查询 text_hash 向量缓存失败,hash={}, configCode={}, error={}",
             log.error("查询 text_hash 向量缓存失败,hash={}, configCode={}, error={}",
@@ -351,17 +233,16 @@ public class VectorizeServiceImpl implements VectorizeService {
     }
     }
 
 
     @Override
     @Override
-    public int batchSaveVectors(List<DeconstructContentVector> vectors) {
+    public int batchSaveVectors(List<ContentVector> vectors) {
         if (CollectionUtils.isEmpty(vectors)) {
         if (CollectionUtils.isEmpty(vectors)) {
             return 0;
             return 0;
         }
         }
 
 
         int count = 0;
         int count = 0;
-        for (DeconstructContentVector vector : vectors) {
+        for (ContentVector vector : vectors) {
             try {
             try {
-                // 存储到 pgvector
-                String embedding = vector.getVectorData();
-                // vectorData 是 JSON 数组格式 "[0.1,0.2,...]",pgvector 也接受这种格式
+                // 存储到 pgvector,embedding 是 JSON 数组格式 "[0.1,0.2,...]"
+                String embedding = vector.getEmbedding();
                 pgContentVectorMapper.upsertWithEmbedding(
                 pgContentVectorMapper.upsertWithEmbedding(
                         vector.getContentId(),
                         vector.getContentId(),
                         vector.getTaskId(),
                         vector.getTaskId(),
@@ -385,80 +266,19 @@ public class VectorizeServiceImpl implements VectorizeService {
         return count;
         return count;
     }
     }
 
 
-    /**
-     * 根据 contentId 查询向量列表(从 pgvector 查询)
-     */
     @Override
     @Override
-    public List<DeconstructContentVector> getVectorsByContentId(Long contentId) {
-        List<ContentVector> pgVectors = pgContentVectorMapper.selectByContentId(contentId);
-        return convertToDeconstructVectors(pgVectors);
+    public List<ContentVector> getVectorsByContentId(Long contentId) {
+        return pgContentVectorMapper.selectByContentId(contentId);
     }
     }
 
 
     @Override
     @Override
-    public List<DeconstructContentVector> getVectorsByContentId(Long contentId, String configCode) {
-        List<ContentVector> pgVectors = pgContentVectorMapper.selectByContentIdAndConfigCode(contentId, configCode);
-        return convertToDeconstructVectors(pgVectors);
+    public List<ContentVector> getVectorsByContentId(Long contentId, String configCode) {
+        return pgContentVectorMapper.selectByContentIdAndConfigCode(contentId, configCode);
     }
     }
 
 
     @Override
     @Override
-    public List<DeconstructContentVector> getVectorsByField(Long contentId, String sourceField) {
-        List<ContentVector> pgVectors = pgContentVectorMapper.selectByContentIdAndField(contentId, sourceField);
-        return convertToDeconstructVectors(pgVectors);
-    }
-
-    /**
-     * 将 pgvector ContentVector 转换为 DeconstructContentVector(兼容现有接口)
-     */
-    private List<DeconstructContentVector> convertToDeconstructVectors(List<ContentVector> pgVectors) {
-        if (CollectionUtils.isEmpty(pgVectors)) {
-            return new ArrayList<>();
-        }
-        return pgVectors.stream().map(pv -> {
-            DeconstructContentVector dcv = new DeconstructContentVector();
-            dcv.setId(pv.getId());
-            dcv.setContentId(pv.getContentId());
-            dcv.setTaskId(pv.getTaskId());
-            dcv.setConfigCode(pv.getConfigCode());
-            dcv.setSourceField(pv.getSourceField());
-            dcv.setSourcePath(pv.getSourcePath());
-            dcv.setTextHash(pv.getTextHash());
-            dcv.setEmbeddingModel(pv.getEmbeddingModel());
-            dcv.setSegmentIndex(pv.getSegmentIndex());
-            dcv.setSegmentTotal(pv.getSegmentTotal());
-            dcv.setSourceText(pv.getSourceText());
-            // 将 pgvector 格式的 embedding 转为 JSON 数组格式的 vectorData
-            if (StringUtils.hasText(pv.getEmbedding())) {
-                dcv.setVectorData(pv.getEmbedding());
-            }
-            dcv.setCreateTime(pv.getCreatedAt());
-            dcv.setUpdateTime(pv.getUpdatedAt());
-            return dcv;
-        }).collect(Collectors.toList());
+    public List<ContentVector> getVectorsByField(Long contentId, String sourceField) {
+        return pgContentVectorMapper.selectByContentIdAndField(contentId, sourceField);
     }
     }
 
 
-    /**
-     * 解析 pgvector 字符串为 List<Float>
-     * 格式: "[0.1,0.2,...]" 或 pgvector 输出的 "[0.1,0.2,...]"
-     */
-    private List<Float> parseVectorString(String vectorStr) {
-        if (vectorStr == null || vectorStr.isEmpty()) return null;
-        try {
-            String trimmed = vectorStr.trim();
-            if (trimmed.startsWith("[")) {
-                trimmed = trimmed.substring(1);
-            }
-            if (trimmed.endsWith("]")) {
-                trimmed = trimmed.substring(0, trimmed.length() - 1);
-            }
-            String[] parts = trimmed.split(",");
-            List<Float> result = new ArrayList<>(parts.length);
-            for (String part : parts) {
-                result.add(Float.parseFloat(part.trim()));
-            }
-            return result;
-        } catch (Exception e) {
-            log.error("向量字符串解析失败: {}", e.getMessage());
-            return null;
-        }
-    }
 }
 }

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

@@ -16,10 +16,11 @@ import com.tzld.videoVector.model.po.pgVector.DeconstructContent;
 import com.tzld.videoVector.model.po.pgVector.DeconstructContentExample;
 import com.tzld.videoVector.model.po.pgVector.DeconstructContentExample;
 import com.tzld.videoVector.model.po.pgVector.DeconstructVectorConfig;
 import com.tzld.videoVector.model.po.pgVector.DeconstructVectorConfig;
 import com.tzld.videoVector.model.po.pgVector.DeconstructVectorConfigExample;
 import com.tzld.videoVector.model.po.pgVector.DeconstructVectorConfigExample;
-import com.tzld.videoVector.model.po.videoVector.deconstruct.DeconstructContentVector;
+import com.tzld.videoVector.model.vo.VideoMatchResult;
 import com.tzld.videoVector.common.constant.VectorConstants;
 import com.tzld.videoVector.common.constant.VectorConstants;
 import com.tzld.videoVector.service.*;
 import com.tzld.videoVector.service.*;
 import com.tzld.videoVector.util.Md5Util;
 import com.tzld.videoVector.util.Md5Util;
+import com.tzld.videoVector.util.VectorUtils;
 import lombok.extern.slf4j.Slf4j;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 import org.springframework.stereotype.Service;
 import org.springframework.util.StringUtils;
 import org.springframework.util.StringUtils;
@@ -417,7 +418,7 @@ public class VideoSearchServiceImpl implements VideoSearchService {
                 return;
                 return;
             }
             }
             // 检查向量表是否已有该 configCode 的记录
             // 检查向量表是否已有该 configCode 的记录
-            List<DeconstructContentVector> vectors =
+            List<ContentVector> vectors =
                     vectorizeService.getVectorsByContentId(content.getId(), configCode);
                     vectorizeService.getVectorsByContentId(content.getId(), configCode);
             if (vectors != null && !vectors.isEmpty()) {
             if (vectors != null && !vectors.isEmpty()) {
                 log.debug("triggerVectorizeIfNeeded: channelContentId={} 已有 configCode={} 向量,无需触发",
                 log.debug("triggerVectorizeIfNeeded: channelContentId={} 已有 configCode={} 向量,无需触发",
@@ -453,7 +454,7 @@ public class VideoSearchServiceImpl implements VideoSearchService {
      * 异步向量化内容,使用线程池执行,避免阻塞接口响应
      * 异步向量化内容,使用线程池执行,避免阻塞接口响应
      */
      */
     @Override
     @Override
-    public List<Object> matchTopNVideo(MatchTopNVideoParam param) {
+    public List<VideoMatchResult> matchTopNVideo(MatchTopNVideoParam param) {
         if (param == null) {
         if (param == null) {
             log.error("matchTopNVideo 参数为空");
             log.error("matchTopNVideo 参数为空");
             return Collections.emptyList();
             return Collections.emptyList();
@@ -492,7 +493,7 @@ public class VideoSearchServiceImpl implements VideoSearchService {
         }
         }
 
 
         // 对每个配置分别搜索,各自取 topN 后汇总返回
         // 对每个配置分别搜索,各自取 topN 后汇总返回
-        List<Object> result = new ArrayList<>();
+        List<VideoMatchResult> result = new ArrayList<>();
         int candidateSize = topN * 3;
         int candidateSize = topN * 3;
 
 
         // 缓存 embeddingModel -> queryVector,避免相同模型重复 embedding
         // 缓存 embeddingModel -> queryVector,避免相同模型重复 embedding
@@ -517,7 +518,7 @@ public class VideoSearchServiceImpl implements VideoSearchService {
                 }
                 }
 
 
                 // 多点模式:复合ID解码为真实videoId,同一videoId去重保留最高分
                 // 多点模式:复合ID解码为真实videoId,同一videoId去重保留最高分
-                boolean multiPoint = isMultiPointConfig(config);
+                boolean multiPoint = VectorUtils.isMultiPointConfig(config);
                 if (multiPoint) {
                 if (multiPoint) {
                     matches = decodeAndDeduplicateMultiPointMatches(matches, cfgCode);
                     matches = decodeAndDeduplicateMultiPointMatches(matches, cfgCode);
                 } else {
                 } else {
@@ -530,13 +531,9 @@ public class VideoSearchServiceImpl implements VideoSearchService {
                 // 每个配置独立进行审核过滤并取各自的 topN
                 // 每个配置独立进行审核过滤并取各自的 topN
                 List<VideoMatch> filteredMatches = filterByAuditStatus(matches, topN);
                 List<VideoMatch> filteredMatches = filterByAuditStatus(matches, topN);
 
 
-                // 转化为返回格式
+                // 转化为强类型返回格式
                 for (VideoMatch match : filteredMatches) {
                 for (VideoMatch match : filteredMatches) {
-                    JSONObject item = new JSONObject();
-                    item.put("configCode", cfgCode);
-                    item.put("videoId", match.getVideoId());
-                    item.put("score", match.getScore());
-                    result.add(item);
+                    result.add(new VideoMatchResult(cfgCode, match.getVideoId(), match.getScore()));
                 }
                 }
 
 
                 log.info("配置 {} 搜索完成,返回 {} 条结果", cfgCode, filteredMatches.size());
                 log.info("配置 {} 搜索完成,返回 {} 条结果", cfgCode, filteredMatches.size());
@@ -553,7 +550,7 @@ public class VideoSearchServiceImpl implements VideoSearchService {
     /**
     /**
      * 降级的原始匹配逻辑(configCode 在数据库中无记录时使用)
      * 降级的原始匹配逻辑(configCode 在数据库中无记录时使用)
      */
      */
-    private List<Object> matchTopNVideoLegacy(MatchTopNVideoParam param, String configCode, int topN) {
+    private List<VideoMatchResult> matchTopNVideoLegacy(MatchTopNVideoParam param, String configCode, int topN) {
         List<Float> queryVector = resolveQueryVector(param);
         List<Float> queryVector = resolveQueryVector(param);
         if (queryVector == null || queryVector.isEmpty()) {
         if (queryVector == null || queryVector.isEmpty()) {
             log.error("matchTopNVideo 无法获取查询向量,param={}", param);
             log.error("matchTopNVideo 无法获取查询向量,param={}", param);
@@ -562,19 +559,17 @@ public class VideoSearchServiceImpl implements VideoSearchService {
         int candidateSize = topN * 3;
         int candidateSize = topN * 3;
         List<VideoMatch> matches = vectorStoreService.searchTopN(configCode, queryVector, candidateSize);
         List<VideoMatch> matches = vectorStoreService.searchTopN(configCode, queryVector, candidateSize);
         List<VideoMatch> filteredMatches = filterByAuditStatus(matches, topN);
         List<VideoMatch> filteredMatches = filterByAuditStatus(matches, topN);
-        List<Object> result = new ArrayList<>(filteredMatches.size());
+        List<VideoMatchResult> result = new ArrayList<>(filteredMatches.size());
         for (VideoMatch match : filteredMatches) {
         for (VideoMatch match : filteredMatches) {
-            JSONObject item = new JSONObject();
-            item.put("videoId", match.getVideoId());
-            item.put("score", match.getScore());
-            item.put("configCode", configCode);
-            result.add(item);
+            result.add(new VideoMatchResult(configCode, match.getVideoId(), match.getScore()));
         }
         }
         return result;
         return result;
     }
     }
 
 
     /**
     /**
-     * 根据具体配置解析查询向量
+     * 根据具体配置解析查询向量(含降级逻辑)
+     * 优先级:直接传入 queryVector > channelContentId 历史向量 > queryText 向量化
+     * 任意一级命中则直接返回;channelContentId 查不到时自动降级到 queryText
      * 相同 embeddingModel 的配置共享同一个查询向量,避免重复 embedding
      * 相同 embeddingModel 的配置共享同一个查询向量,避免重复 embedding
      */
      */
     private List<Float> resolveQueryVectorForConfig(MatchTopNVideoParam param,
     private List<Float> resolveQueryVectorForConfig(MatchTopNVideoParam param,
@@ -585,7 +580,19 @@ public class VideoSearchServiceImpl implements VideoSearchService {
             return param.getQueryVector();
             return param.getQueryVector();
         }
         }
 
 
-        // 2. queryText 向量化:按 embeddingModel 缓存
+        // 2. channelContentId 历史向量(查不到时降级到 queryText)
+        if (StringUtils.hasText(param.getChannelContentId())) {
+            List<Float> cached = getVectorByChannelContentId(param.getChannelContentId(), config.getConfigCode());
+            if (cached != null && !cached.isEmpty()) {
+                log.info("配置 {} 命中 channelContentId 历史向量,channelContentId={}",
+                        config.getConfigCode(), param.getChannelContentId());
+                return cached;
+            }
+            log.info("配置 {} channelContentId={} 未命中历史向量,降级到 queryText",
+                    config.getConfigCode(), param.getChannelContentId());
+        }
+
+        // 3. queryText 向量化:按 embeddingModel 缓存
         if (StringUtils.hasText(param.getQueryText())) {
         if (StringUtils.hasText(param.getQueryText())) {
             String embeddingModel = config.getEmbeddingModel();
             String embeddingModel = config.getEmbeddingModel();
             String cacheKey = embeddingModel != null ? embeddingModel : "__default__";
             String cacheKey = embeddingModel != null ? embeddingModel : "__default__";
@@ -614,21 +621,9 @@ public class VideoSearchServiceImpl implements VideoSearchService {
             return null;
             return null;
         }
         }
 
 
-        // 3. channelContentId 历史向量
-        if (StringUtils.hasText(param.getChannelContentId())) {
-            return getVectorByChannelContentId(param.getChannelContentId(), config.getConfigCode());
-        }
-
         return null;
         return null;
     }
     }
 
 
-    /**
-     * 判断配置是否为多点模式(有 extract_rule 配置)
-     */
-    private static boolean isMultiPointConfig(DeconstructVectorConfig config) {
-        return config != null && StringUtils.hasText(config.getExtractRule());
-    }
-
     /**
     /**
      * 多点模式下将复合ID解码为真实videoId,并对同一videoId去重(保留最高分)
      * 多点模式下将复合ID解码为真实videoId,并对同一videoId去重(保留最高分)
      */
      */
@@ -743,7 +738,7 @@ public class VideoSearchServiceImpl implements VideoSearchService {
                 cached = pgContentVectorMapper.selectByTextHash(textHash);
                 cached = pgContentVectorMapper.selectByTextHash(textHash);
             }
             }
             if (cached != null && StringUtils.hasText(cached.getEmbedding())) {
             if (cached != null && StringUtils.hasText(cached.getEmbedding())) {
-                return parseVectorString(cached.getEmbedding());
+                return VectorUtils.parseVectorString(cached.getEmbedding());
             }
             }
         } catch (Exception e) {
         } catch (Exception e) {
             log.error("按 text_hash 查询向量失败,hash={}, configCode={}, error={}", textHash, configCode, e.getMessage(), e);
             log.error("按 text_hash 查询向量失败,hash={}, configCode={}, error={}", textHash, configCode, e.getMessage(), e);
@@ -751,57 +746,37 @@ public class VideoSearchServiceImpl implements VideoSearchService {
         return null;
         return null;
     }
     }
 
 
-    /**
-     * 解析 pgvector 字符串为 List<Float>
-     */
-    private List<Float> parseVectorString(String vectorStr) {
-        if (vectorStr == null || vectorStr.isEmpty()) return null;
-        try {
-            String trimmed = vectorStr.trim();
-            if (trimmed.startsWith("[")) {
-                trimmed = trimmed.substring(1);
-            }
-            if (trimmed.endsWith("]")) {
-                trimmed = trimmed.substring(0, trimmed.length() - 1);
-            }
-            String[] parts = trimmed.split(",");
-            List<Float> result = new ArrayList<>(parts.length);
-            for (String part : parts) {
-                result.add(Float.parseFloat(part.trim()));
-            }
-            return result;
-        } catch (Exception e) {
-            log.error("向量字符串解析失败: {}", e.getMessage());
-            return null;
-        }
-    }
-
     /**
     /**
      * 通过 channelContentId 查询历史向量化结果,有则直接复用,避免重复调用 embedding
      * 通过 channelContentId 查询历史向量化结果,有则直接复用,避免重复调用 embedding
+     * 直接从 PG content_vectors 表查询,不再依赖 MySQL
      */
      */
     private List<Float> getVectorByChannelContentId(String channelContentId, String configCode) {
     private List<Float> getVectorByChannelContentId(String channelContentId, String configCode) {
         try {
         try {
-            // 查询 deconstruct_content
+            // 查询 deconstruct_content(PG)
             DeconstructContent content = getDeconstructContentByChannelContentId(channelContentId);
             DeconstructContent content = getDeconstructContentByChannelContentId(channelContentId);
             if (content == null || content.getId() == null) {
             if (content == null || content.getId() == null) {
                 log.info("未找到 channelContentId={} 对应的解构内容", channelContentId);
                 log.info("未找到 channelContentId={} 对应的解构内容", channelContentId);
                 return null;
                 return null;
             }
             }
-            // 查询 deconstruct_content_vector(按 configCode 过滤)
-            List<DeconstructContentVector> vectors = vectorizeService.getVectorsByContentId(content.getId(), configCode);
-            if (vectors == null || vectors.isEmpty()) {
+            // 直接从 PG content_vectors 查询向量(按 configCode 过滤)
+            List<ContentVector> pgVectors;
+            if (StringUtils.hasText(configCode)) {
+                pgVectors = pgContentVectorMapper.selectByContentIdAndConfigCode(content.getId(), configCode);
+            } else {
+                pgVectors = pgContentVectorMapper.selectByContentId(content.getId());
+            }
+            if (pgVectors == null || pgVectors.isEmpty()) {
                 log.info("channelContentId={} 尚无历史向量化结果,contentId={}, configCode={}",
                 log.info("channelContentId={} 尚无历史向量化结果,contentId={}, configCode={}",
                         channelContentId, content.getId(), configCode);
                         channelContentId, content.getId(), configCode);
                 return null;
                 return null;
             }
             }
             // 使用第一条向量数据
             // 使用第一条向量数据
-            DeconstructContentVector vector = vectors.get(0);
-            String vectorDataJson = vector.getVectorData();
-            if (!StringUtils.hasText(vectorDataJson)) {
+            ContentVector vector = pgVectors.get(0);
+            if (!StringUtils.hasText(vector.getEmbedding())) {
                 return null;
                 return null;
             }
             }
-            List<Float> vectorData = com.alibaba.fastjson.JSON.parseArray(vectorDataJson, Float.class);
-            log.info("复用历史向量化结果,channelContentId={}, contentId={}, configCode={}, sourceField={}, 向量维度={}",
+            List<Float> vectorData = VectorUtils.parseVectorString(vector.getEmbedding());
+            log.info("复用历史向量化结果(PG),channelContentId={}, contentId={}, configCode={}, sourceField={}, 向量维度={}",
                     channelContentId, content.getId(), configCode, vector.getSourceField(),
                     channelContentId, content.getId(), configCode, vector.getSourceField(),
                     vectorData != null ? vectorData.size() : 0);
                     vectorData != null ? vectorData.size() : 0);
             return vectorData;
             return vectorData;

+ 281 - 0
core/src/main/java/com/tzld/videoVector/util/VectorUtils.java

@@ -0,0 +1,281 @@
+package com.tzld.videoVector.util;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.tzld.videoVector.model.po.pgVector.DeconstructVectorConfig;
+import com.tzld.videoVector.common.constant.VectorConstants;
+import org.springframework.util.StringUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 向量化公共工具方法
+ * 集中管理 parseVectorString、extractFromJson、parseJsonPath、isMultiPointConfig 等通用逻辑
+ */
+public final class VectorUtils {
+
+    private VectorUtils() {
+    }
+
+    // ========================== 向量字符串解析 ==========================
+
+    /**
+     * 解析 pgvector 字符串为 List<Float>
+     * 格式: "[0.1,0.2,...]"
+     */
+    public static List<Float> parseVectorString(String vectorStr) {
+        if (vectorStr == null || vectorStr.isEmpty()) return null;
+        try {
+            String trimmed = vectorStr.trim();
+            if (trimmed.startsWith("[")) {
+                trimmed = trimmed.substring(1);
+            }
+            if (trimmed.endsWith("]")) {
+                trimmed = trimmed.substring(0, trimmed.length() - 1);
+            }
+            String[] parts = trimmed.split(",");
+            List<Float> result = new ArrayList<>(parts.length);
+            for (String part : parts) {
+                result.add(Float.parseFloat(part.trim()));
+            }
+            return result;
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    // ========================== 多点模式判断 ==========================
+
+    /**
+     * 判断配置是否为多点模式(有 extract_rule 配置)
+     */
+    public static boolean isMultiPointConfig(DeconstructVectorConfig config) {
+        return config != null && StringUtils.hasText(config.getExtractRule());
+    }
+
+    // ========================== 多点ID编解码 ==========================
+
+    /**
+     * 编码多点复合ID
+     * 复合ID = videoId * MULTI_POINT_FACTOR + pointIndex
+     *
+     * <p><b>注意:当前方案存在上限约束</b>(每个视频最多 {@code MULTI_POINT_FACTOR - 1} 个向量点),
+     * 若未来单视频向量点数超过此上限,需改为在 video_vectors 表中增加 point_index 列来替代复合ID方案。
+     *
+     * @param videoId   原始视频ID
+     * @param pointIndex 点索引(0 ~ MULTI_POINT_FACTOR - 1)
+     * @return 复合ID
+     * @throws IllegalArgumentException 如果 pointIndex 超出范围
+     */
+    public static long encodeMultiPointId(long videoId, int pointIndex) {
+        if (pointIndex < 0 || pointIndex >= VectorConstants.MULTI_POINT_FACTOR) {
+            throw new IllegalArgumentException(
+                    "pointIndex 超出范围: " + pointIndex + 
+                    ", 允许范围 [0, " + (VectorConstants.MULTI_POINT_FACTOR - 1) + "]");
+        }
+        return videoId * VectorConstants.MULTI_POINT_FACTOR + pointIndex;
+    }
+
+    /**
+     * 从复合ID解码出原始视频ID
+     * 解码公式: realVideoId = compositeId / MULTI_POINT_FACTOR
+     *
+     * @param compositeId 复合ID
+     * @return 原始视频ID
+     */
+    public static long decodeVideoId(long compositeId) {
+        return compositeId / VectorConstants.MULTI_POINT_FACTOR;
+    }
+
+    // ========================== JSON 数组项提取 ==========================
+
+    /**
+     * 从JSON中提取数组项对象列表(用于多点模式置信度过滤)
+     * 路径格式:$.a.b.c[*],以 [*] 结尾表示提取数组中的 JSONObject 列表
+     *
+     * @param json       原始JSONObject
+     * @param sourcePath JSONPath,如 $.final_normalization_rebuild.keypoint_final.最终关键点列表[*]
+     * @return 提取的JSONObject列表
+     */
+    public static List<JSONObject> extractArrayItemsFromJson(JSONObject json, String sourcePath) {
+        List<JSONObject> items = new ArrayList<>();
+
+        if (json == null || !StringUtils.hasText(sourcePath)) {
+            return items;
+        }
+
+        try {
+            if (!sourcePath.startsWith("$.")) {
+                return items;
+            }
+
+            String pathContent = sourcePath.substring(2);
+            // 路径应以 [*] 结尾
+            if (!pathContent.endsWith("[*]")) {
+                return items;
+            }
+
+            // 去掉末尾的 [*],找到数组所在的父路径和数组字段名
+            String pathWithoutWildcard = pathContent.substring(0, pathContent.length() - 3);
+            List<String> parts = parseJsonPath(pathWithoutWildcard);
+
+            // 逐层访问到数组字段的父对象
+            Object current = json;
+            for (int i = 0; i < parts.size() - 1; i++) {
+                if (current instanceof JSONObject) {
+                    current = ((JSONObject) current).get(parts.get(i));
+                } else {
+                    return items;
+                }
+            }
+
+            // 获取数组
+            if (current instanceof JSONObject && !parts.isEmpty()) {
+                String arrayKey = parts.get(parts.size() - 1);
+                JSONArray array = ((JSONObject) current).getJSONArray(arrayKey);
+                if (array != null) {
+                    for (int i = 0; i < array.size(); i++) {
+                        JSONObject item = array.getJSONObject(i);
+                        if (item != null) {
+                            items.add(item);
+                        }
+                    }
+                }
+            }
+
+        } catch (Exception e) {
+            // silently ignore, consistent with other utility methods
+        }
+
+        return items;
+    }
+
+    // ========================== JSON 路径提取 ==========================
+
+    /**
+     * 从JSON中提取文本
+     * 支持路径格式:$.final_normalization_rebuild.topic_fusion_result.最终选题.选题
+     * 支持中文 key 和数组路径如 topics[*]
+     */
+    public static List<String> extractFromJson(String json, String path) {
+        List<String> results = new ArrayList<>();
+
+        if (!StringUtils.hasText(json) || !StringUtils.hasText(path)) {
+            return results;
+        }
+
+        try {
+            JSONObject jsonObject = JSON.parseObject(json);
+            if (jsonObject == null) {
+                return results;
+            }
+            results.addAll(extractFromJson(jsonObject, path));
+        } catch (Exception e) {
+            // silently ignore
+        }
+
+        return results;
+    }
+
+    /**
+     * 从JSONObject中提取文本(重载)
+     */
+    public static List<String> extractFromJson(JSONObject json, String path) {
+        List<String> results = new ArrayList<>();
+
+        if (json == null || !StringUtils.hasText(path)) {
+            return results;
+        }
+
+        try {
+            if (path.startsWith("$.")) {
+                String pathContent = path.substring(2);
+                List<String> parts = parseJsonPath(pathContent);
+                Object current = json;
+
+                for (int i = 0; i < parts.size(); i++) {
+                    String part = parts.get(i);
+                    if (current == null) {
+                        break;
+                    }
+
+                    // 处理数组路径(如 topics[*])
+                    if (part.endsWith("[*]")) {
+                        String arrayKey = part.substring(0, part.length() - 3);
+                        if (current instanceof JSONObject) {
+                            JSONArray array = ((JSONObject) current).getJSONArray(arrayKey);
+                            if (array != null) {
+                                List<String> remainingParts = parts.subList(i + 1, parts.size());
+                                String remainingPath = String.join(".", remainingParts);
+                                for (int j = 0; j < array.size(); j++) {
+                                    Object item = array.get(j);
+                                    if (remainingParts.isEmpty()) {
+                                        if (item instanceof String) {
+                                            results.add((String) item);
+                                        }
+                                    } else {
+                                        results.addAll(extractFromJson(
+                                                JSON.toJSONString(item), "$." + remainingPath));
+                                    }
+                                }
+                            }
+                        }
+                        return results;
+                    } else {
+                        // 普通对象路径(支持中文 key)
+                        if (current instanceof JSONObject) {
+                            current = ((JSONObject) current).get(part);
+                        } else {
+                            break;
+                        }
+                    }
+                }
+
+                // 提取最终值
+                if (current instanceof String) {
+                    results.add((String) current);
+                } else if (current instanceof JSONArray) {
+                    JSONArray array = (JSONArray) current;
+                    for (int i = 0; i < array.size(); i++) {
+                        Object item = array.get(i);
+                        if (item instanceof String) {
+                            results.add((String) item);
+                        }
+                    }
+                }
+            }
+        } catch (Exception e) {
+            // silently ignore
+        }
+
+        return results;
+    }
+
+    /**
+     * 解析 JSONPath 路径,支持中文 key
+     * 例如:final_normalization_rebuild.topic_fusion_result.最终选题.选题
+     */
+    public static List<String> parseJsonPath(String pathContent) {
+        List<String> parts = new ArrayList<>();
+        StringBuilder current = new StringBuilder();
+
+        for (int i = 0; i < pathContent.length(); i++) {
+            char c = pathContent.charAt(i);
+            if (c == '.') {
+                if (current.length() > 0) {
+                    parts.add(current.toString());
+                    current = new StringBuilder();
+                }
+            } else {
+                current.append(c);
+            }
+        }
+        if (current.length() > 0) {
+            parts.add(current.toString());
+        }
+
+        return parts;
+    }
+}

+ 2 - 2
core/src/main/resources/generator/mybatis-vector-generator-config.xml

@@ -46,9 +46,9 @@
             <property name="enableSubPackages" value="true"/>
             <property name="enableSubPackages" value="true"/>
         </javaClientGenerator>
         </javaClientGenerator>
 
 
-<!--        <table tableName="deconstruct_content" domainObjectName="" alias=""/>-->
+<!--        <table tableName="deconstruct_content" domainObjectName="MysqlDeconstructContent" alias=""/>-->
         <table tableName="deconstruct_content_vector" domainObjectName="" alias=""/>
         <table tableName="deconstruct_content_vector" domainObjectName="" alias=""/>
-<!--        <table tableName="deconstruct_vector_config" domainObjectName="" alias=""/>-->
+<!--        <table tableName="deconstruct_vector_config" domainObjectName="MysqlDeconstructVectorConfig" alias=""/>-->
     </context>
     </context>
 
 
 </generatorConfiguration>
 </generatorConfiguration>

+ 12 - 12
core/src/main/resources/mapper/videoVector/deconstruct/DeconstructContentMapper.xml → core/src/main/resources/mapper/videoVector/deconstruct/MysqlDeconstructContentMapper.xml

@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <?xml version="1.0" encoding="UTF-8"?>
 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
-<mapper namespace="com.tzld.videoVector.dao.mapper.videoVector.deconstruct.DeconstructContentMapper">
-  <resultMap id="BaseResultMap" type="com.tzld.videoVector.model.po.videoVector.deconstruct.DeconstructContent">
+<mapper namespace="com.tzld.videoVector.dao.mapper.videoVector.deconstruct.MysqlDeconstructContentMapper">
+  <resultMap id="BaseResultMap" type="com.tzld.videoVector.model.po.videoVector.deconstruct.MysqlDeconstructContent">
     <!--
     <!--
       WARNING - @mbg.generated
       WARNING - @mbg.generated
       This element is automatically generated by MyBatis Generator, do not modify.
       This element is automatically generated by MyBatis Generator, do not modify.
@@ -25,7 +25,7 @@
     <result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
     <result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
     <result column="update_time" jdbcType="TIMESTAMP" property="updateTime" />
     <result column="update_time" jdbcType="TIMESTAMP" property="updateTime" />
   </resultMap>
   </resultMap>
-  <resultMap extends="BaseResultMap" id="ResultMapWithBLOBs" type="com.tzld.videoVector.model.po.videoVector.deconstruct.DeconstructContent">
+  <resultMap extends="BaseResultMap" id="ResultMapWithBLOBs" type="com.tzld.videoVector.model.po.videoVector.deconstruct.MysqlDeconstructContent">
     <!--
     <!--
       WARNING - @mbg.generated
       WARNING - @mbg.generated
       This element is automatically generated by MyBatis Generator, do not modify.
       This element is automatically generated by MyBatis Generator, do not modify.
@@ -120,7 +120,7 @@
     -->
     -->
     body_text, result_json
     body_text, result_json
   </sql>
   </sql>
-  <select id="selectByExampleWithBLOBs" parameterType="com.tzld.videoVector.model.po.videoVector.deconstruct.DeconstructContentExample" resultMap="ResultMapWithBLOBs">
+  <select id="selectByExampleWithBLOBs" parameterType="com.tzld.videoVector.model.po.videoVector.deconstruct.MysqlDeconstructContentExample" resultMap="ResultMapWithBLOBs">
     <!--
     <!--
       WARNING - @mbg.generated
       WARNING - @mbg.generated
       This element is automatically generated by MyBatis Generator, do not modify.
       This element is automatically generated by MyBatis Generator, do not modify.
@@ -141,7 +141,7 @@
       order by ${orderByClause}
       order by ${orderByClause}
     </if>
     </if>
   </select>
   </select>
-  <select id="selectByExample" parameterType="com.tzld.videoVector.model.po.videoVector.deconstruct.DeconstructContentExample" resultMap="BaseResultMap">
+  <select id="selectByExample" parameterType="com.tzld.videoVector.model.po.videoVector.deconstruct.MysqlDeconstructContentExample" resultMap="BaseResultMap">
     <!--
     <!--
       WARNING - @mbg.generated
       WARNING - @mbg.generated
       This element is automatically generated by MyBatis Generator, do not modify.
       This element is automatically generated by MyBatis Generator, do not modify.
@@ -182,7 +182,7 @@
     delete from deconstruct_content
     delete from deconstruct_content
     where id = #{id,jdbcType=BIGINT}
     where id = #{id,jdbcType=BIGINT}
   </delete>
   </delete>
-  <delete id="deleteByExample" parameterType="com.tzld.videoVector.model.po.videoVector.deconstruct.DeconstructContentExample">
+  <delete id="deleteByExample" parameterType="com.tzld.videoVector.model.po.videoVector.deconstruct.MysqlDeconstructContentExample">
     <!--
     <!--
       WARNING - @mbg.generated
       WARNING - @mbg.generated
       This element is automatically generated by MyBatis Generator, do not modify.
       This element is automatically generated by MyBatis Generator, do not modify.
@@ -193,7 +193,7 @@
       <include refid="Example_Where_Clause" />
       <include refid="Example_Where_Clause" />
     </if>
     </if>
   </delete>
   </delete>
-  <insert id="insert" parameterType="com.tzld.videoVector.model.po.videoVector.deconstruct.DeconstructContent">
+  <insert id="insert" parameterType="com.tzld.videoVector.model.po.videoVector.deconstruct.MysqlDeconstructContent">
     <!--
     <!--
       WARNING - @mbg.generated
       WARNING - @mbg.generated
       This element is automatically generated by MyBatis Generator, do not modify.
       This element is automatically generated by MyBatis Generator, do not modify.
@@ -214,7 +214,7 @@
       #{createTime,jdbcType=TIMESTAMP}, #{updateTime,jdbcType=TIMESTAMP}, #{bodyText,jdbcType=LONGVARCHAR}, 
       #{createTime,jdbcType=TIMESTAMP}, #{updateTime,jdbcType=TIMESTAMP}, #{bodyText,jdbcType=LONGVARCHAR}, 
       #{resultJson,jdbcType=LONGVARCHAR})
       #{resultJson,jdbcType=LONGVARCHAR})
   </insert>
   </insert>
-  <insert id="insertSelective" parameterType="com.tzld.videoVector.model.po.videoVector.deconstruct.DeconstructContent">
+  <insert id="insertSelective" parameterType="com.tzld.videoVector.model.po.videoVector.deconstruct.MysqlDeconstructContent">
     <!--
     <!--
       WARNING - @mbg.generated
       WARNING - @mbg.generated
       This element is automatically generated by MyBatis Generator, do not modify.
       This element is automatically generated by MyBatis Generator, do not modify.
@@ -340,7 +340,7 @@
       </if>
       </if>
     </trim>
     </trim>
   </insert>
   </insert>
-  <select id="countByExample" parameterType="com.tzld.videoVector.model.po.videoVector.deconstruct.DeconstructContentExample" resultType="java.lang.Long">
+  <select id="countByExample" parameterType="com.tzld.videoVector.model.po.videoVector.deconstruct.MysqlDeconstructContentExample" resultType="java.lang.Long">
     <!--
     <!--
       WARNING - @mbg.generated
       WARNING - @mbg.generated
       This element is automatically generated by MyBatis Generator, do not modify.
       This element is automatically generated by MyBatis Generator, do not modify.
@@ -479,7 +479,7 @@
       <include refid="Update_By_Example_Where_Clause" />
       <include refid="Update_By_Example_Where_Clause" />
     </if>
     </if>
   </update>
   </update>
-  <update id="updateByPrimaryKeySelective" parameterType="com.tzld.videoVector.model.po.videoVector.deconstruct.DeconstructContent">
+  <update id="updateByPrimaryKeySelective" parameterType="com.tzld.videoVector.model.po.videoVector.deconstruct.MysqlDeconstructContent">
     <!--
     <!--
       WARNING - @mbg.generated
       WARNING - @mbg.generated
       This element is automatically generated by MyBatis Generator, do not modify.
       This element is automatically generated by MyBatis Generator, do not modify.
@@ -544,7 +544,7 @@
     </set>
     </set>
     where id = #{id,jdbcType=BIGINT}
     where id = #{id,jdbcType=BIGINT}
   </update>
   </update>
-  <update id="updateByPrimaryKeyWithBLOBs" parameterType="com.tzld.videoVector.model.po.videoVector.deconstruct.DeconstructContent">
+  <update id="updateByPrimaryKeyWithBLOBs" parameterType="com.tzld.videoVector.model.po.videoVector.deconstruct.MysqlDeconstructContent">
     <!--
     <!--
       WARNING - @mbg.generated
       WARNING - @mbg.generated
       This element is automatically generated by MyBatis Generator, do not modify.
       This element is automatically generated by MyBatis Generator, do not modify.
@@ -571,7 +571,7 @@
       result_json = #{resultJson,jdbcType=LONGVARCHAR}
       result_json = #{resultJson,jdbcType=LONGVARCHAR}
     where id = #{id,jdbcType=BIGINT}
     where id = #{id,jdbcType=BIGINT}
   </update>
   </update>
-  <update id="updateByPrimaryKey" parameterType="com.tzld.videoVector.model.po.videoVector.deconstruct.DeconstructContent">
+  <update id="updateByPrimaryKey" parameterType="com.tzld.videoVector.model.po.videoVector.deconstruct.MysqlDeconstructContent">
     <!--
     <!--
       WARNING - @mbg.generated
       WARNING - @mbg.generated
       This element is automatically generated by MyBatis Generator, do not modify.
       This element is automatically generated by MyBatis Generator, do not modify.

+ 12 - 12
core/src/main/resources/mapper/videoVector/deconstruct/DeconstructContentVectorMapper.xml → core/src/main/resources/mapper/videoVector/deconstruct/MysqlDeconstructContentVectorMapper.xml

@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <?xml version="1.0" encoding="UTF-8"?>
 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
-<mapper namespace="com.tzld.videoVector.dao.mapper.videoVector.deconstruct.DeconstructContentVectorMapper">
-  <resultMap id="BaseResultMap" type="com.tzld.videoVector.model.po.videoVector.deconstruct.DeconstructContentVector">
+<mapper namespace="com.tzld.videoVector.dao.mapper.videoVector.deconstruct.MysqlDeconstructContentVectorMapper">
+  <resultMap id="BaseResultMap" type="com.tzld.videoVector.model.po.videoVector.deconstruct.MysqlDeconstructContentVector">
     <!--
     <!--
       WARNING - @mbg.generated
       WARNING - @mbg.generated
       This element is automatically generated by MyBatis Generator, do not modify.
       This element is automatically generated by MyBatis Generator, do not modify.
@@ -21,7 +21,7 @@
     <result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
     <result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
     <result column="update_time" jdbcType="TIMESTAMP" property="updateTime" />
     <result column="update_time" jdbcType="TIMESTAMP" property="updateTime" />
   </resultMap>
   </resultMap>
-  <resultMap extends="BaseResultMap" id="ResultMapWithBLOBs" type="com.tzld.videoVector.model.po.videoVector.deconstruct.DeconstructContentVector">
+  <resultMap extends="BaseResultMap" id="ResultMapWithBLOBs" type="com.tzld.videoVector.model.po.videoVector.deconstruct.MysqlDeconstructContentVector">
     <!--
     <!--
       WARNING - @mbg.generated
       WARNING - @mbg.generated
       This element is automatically generated by MyBatis Generator, do not modify.
       This element is automatically generated by MyBatis Generator, do not modify.
@@ -115,7 +115,7 @@
     -->
     -->
     vector_data, source_text
     vector_data, source_text
   </sql>
   </sql>
-  <select id="selectByExampleWithBLOBs" parameterType="com.tzld.videoVector.model.po.videoVector.deconstruct.DeconstructContentVectorExample" resultMap="ResultMapWithBLOBs">
+  <select id="selectByExampleWithBLOBs" parameterType="com.tzld.videoVector.model.po.videoVector.deconstruct.MysqlDeconstructContentVectorExample" resultMap="ResultMapWithBLOBs">
     <!--
     <!--
       WARNING - @mbg.generated
       WARNING - @mbg.generated
       This element is automatically generated by MyBatis Generator, do not modify.
       This element is automatically generated by MyBatis Generator, do not modify.
@@ -136,7 +136,7 @@
       order by ${orderByClause}
       order by ${orderByClause}
     </if>
     </if>
   </select>
   </select>
-  <select id="selectByExample" parameterType="com.tzld.videoVector.model.po.videoVector.deconstruct.DeconstructContentVectorExample" resultMap="BaseResultMap">
+  <select id="selectByExample" parameterType="com.tzld.videoVector.model.po.videoVector.deconstruct.MysqlDeconstructContentVectorExample" resultMap="BaseResultMap">
     <!--
     <!--
       WARNING - @mbg.generated
       WARNING - @mbg.generated
       This element is automatically generated by MyBatis Generator, do not modify.
       This element is automatically generated by MyBatis Generator, do not modify.
@@ -177,7 +177,7 @@
     delete from deconstruct_content_vector
     delete from deconstruct_content_vector
     where id = #{id,jdbcType=BIGINT}
     where id = #{id,jdbcType=BIGINT}
   </delete>
   </delete>
-  <delete id="deleteByExample" parameterType="com.tzld.videoVector.model.po.videoVector.deconstruct.DeconstructContentVectorExample">
+  <delete id="deleteByExample" parameterType="com.tzld.videoVector.model.po.videoVector.deconstruct.MysqlDeconstructContentVectorExample">
     <!--
     <!--
       WARNING - @mbg.generated
       WARNING - @mbg.generated
       This element is automatically generated by MyBatis Generator, do not modify.
       This element is automatically generated by MyBatis Generator, do not modify.
@@ -188,7 +188,7 @@
       <include refid="Example_Where_Clause" />
       <include refid="Example_Where_Clause" />
     </if>
     </if>
   </delete>
   </delete>
-  <insert id="insert" parameterType="com.tzld.videoVector.model.po.videoVector.deconstruct.DeconstructContentVector">
+  <insert id="insert" parameterType="com.tzld.videoVector.model.po.videoVector.deconstruct.MysqlDeconstructContentVector">
     <!--
     <!--
       WARNING - @mbg.generated
       WARNING - @mbg.generated
       This element is automatically generated by MyBatis Generator, do not modify.
       This element is automatically generated by MyBatis Generator, do not modify.
@@ -207,7 +207,7 @@
       #{updateTime,jdbcType=TIMESTAMP}, #{vectorData,jdbcType=LONGVARCHAR}, #{sourceText,jdbcType=LONGVARCHAR}
       #{updateTime,jdbcType=TIMESTAMP}, #{vectorData,jdbcType=LONGVARCHAR}, #{sourceText,jdbcType=LONGVARCHAR}
       )
       )
   </insert>
   </insert>
-  <insert id="insertSelective" parameterType="com.tzld.videoVector.model.po.videoVector.deconstruct.DeconstructContentVector">
+  <insert id="insertSelective" parameterType="com.tzld.videoVector.model.po.videoVector.deconstruct.MysqlDeconstructContentVector">
     <!--
     <!--
       WARNING - @mbg.generated
       WARNING - @mbg.generated
       This element is automatically generated by MyBatis Generator, do not modify.
       This element is automatically generated by MyBatis Generator, do not modify.
@@ -309,7 +309,7 @@
       </if>
       </if>
     </trim>
     </trim>
   </insert>
   </insert>
-  <select id="countByExample" parameterType="com.tzld.videoVector.model.po.videoVector.deconstruct.DeconstructContentVectorExample" resultType="java.lang.Long">
+  <select id="countByExample" parameterType="com.tzld.videoVector.model.po.videoVector.deconstruct.MysqlDeconstructContentVectorExample" resultType="java.lang.Long">
     <!--
     <!--
       WARNING - @mbg.generated
       WARNING - @mbg.generated
       This element is automatically generated by MyBatis Generator, do not modify.
       This element is automatically generated by MyBatis Generator, do not modify.
@@ -428,7 +428,7 @@
       <include refid="Update_By_Example_Where_Clause" />
       <include refid="Update_By_Example_Where_Clause" />
     </if>
     </if>
   </update>
   </update>
-  <update id="updateByPrimaryKeySelective" parameterType="com.tzld.videoVector.model.po.videoVector.deconstruct.DeconstructContentVector">
+  <update id="updateByPrimaryKeySelective" parameterType="com.tzld.videoVector.model.po.videoVector.deconstruct.MysqlDeconstructContentVector">
     <!--
     <!--
       WARNING - @mbg.generated
       WARNING - @mbg.generated
       This element is automatically generated by MyBatis Generator, do not modify.
       This element is automatically generated by MyBatis Generator, do not modify.
@@ -481,7 +481,7 @@
     </set>
     </set>
     where id = #{id,jdbcType=BIGINT}
     where id = #{id,jdbcType=BIGINT}
   </update>
   </update>
-  <update id="updateByPrimaryKeyWithBLOBs" parameterType="com.tzld.videoVector.model.po.videoVector.deconstruct.DeconstructContentVector">
+  <update id="updateByPrimaryKeyWithBLOBs" parameterType="com.tzld.videoVector.model.po.videoVector.deconstruct.MysqlDeconstructContentVector">
     <!--
     <!--
       WARNING - @mbg.generated
       WARNING - @mbg.generated
       This element is automatically generated by MyBatis Generator, do not modify.
       This element is automatically generated by MyBatis Generator, do not modify.
@@ -504,7 +504,7 @@
       source_text = #{sourceText,jdbcType=LONGVARCHAR}
       source_text = #{sourceText,jdbcType=LONGVARCHAR}
     where id = #{id,jdbcType=BIGINT}
     where id = #{id,jdbcType=BIGINT}
   </update>
   </update>
-  <update id="updateByPrimaryKey" parameterType="com.tzld.videoVector.model.po.videoVector.deconstruct.DeconstructContentVector">
+  <update id="updateByPrimaryKey" parameterType="com.tzld.videoVector.model.po.videoVector.deconstruct.MysqlDeconstructContentVector">
     <!--
     <!--
       WARNING - @mbg.generated
       WARNING - @mbg.generated
       This element is automatically generated by MyBatis Generator, do not modify.
       This element is automatically generated by MyBatis Generator, do not modify.

+ 9 - 9
core/src/main/resources/mapper/videoVector/deconstruct/DeconstructVectorConfigMapper.xml → core/src/main/resources/mapper/videoVector/deconstruct/MysqlDeconstructVectorConfigMapper.xml

@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <?xml version="1.0" encoding="UTF-8"?>
 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
-<mapper namespace="com.tzld.videoVector.dao.mapper.videoVector.deconstruct.DeconstructVectorConfigMapper">
-  <resultMap id="BaseResultMap" type="com.tzld.videoVector.model.po.videoVector.deconstruct.DeconstructVectorConfig">
+<mapper namespace="com.tzld.videoVector.dao.mapper.videoVector.deconstruct.MysqlDeconstructVectorConfigMapper">
+  <resultMap id="BaseResultMap" type="com.tzld.videoVector.model.po.videoVector.deconstruct.MysqlDeconstructVectorConfig">
     <!--
     <!--
       WARNING - @mbg.generated
       WARNING - @mbg.generated
       This element is automatically generated by MyBatis Generator, do not modify.
       This element is automatically generated by MyBatis Generator, do not modify.
@@ -103,7 +103,7 @@
     extract_rule, embedding_model, dimension, max_length, enable_segment, segment_size, 
     extract_rule, embedding_model, dimension, max_length, enable_segment, segment_size, 
     priority, enabled, create_time, update_time
     priority, enabled, create_time, update_time
   </sql>
   </sql>
-  <select id="selectByExample" parameterType="com.tzld.videoVector.model.po.videoVector.deconstruct.DeconstructVectorConfigExample" resultMap="BaseResultMap">
+  <select id="selectByExample" parameterType="com.tzld.videoVector.model.po.videoVector.deconstruct.MysqlDeconstructVectorConfigExample" resultMap="BaseResultMap">
     <!--
     <!--
       WARNING - @mbg.generated
       WARNING - @mbg.generated
       This element is automatically generated by MyBatis Generator, do not modify.
       This element is automatically generated by MyBatis Generator, do not modify.
@@ -142,7 +142,7 @@
     delete from deconstruct_vector_config
     delete from deconstruct_vector_config
     where id = #{id,jdbcType=BIGINT}
     where id = #{id,jdbcType=BIGINT}
   </delete>
   </delete>
-  <delete id="deleteByExample" parameterType="com.tzld.videoVector.model.po.videoVector.deconstruct.DeconstructVectorConfigExample">
+  <delete id="deleteByExample" parameterType="com.tzld.videoVector.model.po.videoVector.deconstruct.MysqlDeconstructVectorConfigExample">
     <!--
     <!--
       WARNING - @mbg.generated
       WARNING - @mbg.generated
       This element is automatically generated by MyBatis Generator, do not modify.
       This element is automatically generated by MyBatis Generator, do not modify.
@@ -153,7 +153,7 @@
       <include refid="Example_Where_Clause" />
       <include refid="Example_Where_Clause" />
     </if>
     </if>
   </delete>
   </delete>
-  <insert id="insert" parameterType="com.tzld.videoVector.model.po.videoVector.deconstruct.DeconstructVectorConfig">
+  <insert id="insert" parameterType="com.tzld.videoVector.model.po.videoVector.deconstruct.MysqlDeconstructVectorConfig">
     <!--
     <!--
       WARNING - @mbg.generated
       WARNING - @mbg.generated
       This element is automatically generated by MyBatis Generator, do not modify.
       This element is automatically generated by MyBatis Generator, do not modify.
@@ -172,7 +172,7 @@
       #{segmentSize,jdbcType=INTEGER}, #{priority,jdbcType=INTEGER}, #{enabled,jdbcType=TINYINT}, 
       #{segmentSize,jdbcType=INTEGER}, #{priority,jdbcType=INTEGER}, #{enabled,jdbcType=TINYINT}, 
       #{createTime,jdbcType=TIMESTAMP}, #{updateTime,jdbcType=TIMESTAMP})
       #{createTime,jdbcType=TIMESTAMP}, #{updateTime,jdbcType=TIMESTAMP})
   </insert>
   </insert>
-  <insert id="insertSelective" parameterType="com.tzld.videoVector.model.po.videoVector.deconstruct.DeconstructVectorConfig">
+  <insert id="insertSelective" parameterType="com.tzld.videoVector.model.po.videoVector.deconstruct.MysqlDeconstructVectorConfig">
     <!--
     <!--
       WARNING - @mbg.generated
       WARNING - @mbg.generated
       This element is automatically generated by MyBatis Generator, do not modify.
       This element is automatically generated by MyBatis Generator, do not modify.
@@ -286,7 +286,7 @@
       </if>
       </if>
     </trim>
     </trim>
   </insert>
   </insert>
-  <select id="countByExample" parameterType="com.tzld.videoVector.model.po.videoVector.deconstruct.DeconstructVectorConfigExample" resultType="java.lang.Long">
+  <select id="countByExample" parameterType="com.tzld.videoVector.model.po.videoVector.deconstruct.MysqlDeconstructVectorConfigExample" resultType="java.lang.Long">
     <!--
     <!--
       WARNING - @mbg.generated
       WARNING - @mbg.generated
       This element is automatically generated by MyBatis Generator, do not modify.
       This element is automatically generated by MyBatis Generator, do not modify.
@@ -389,7 +389,7 @@
       <include refid="Update_By_Example_Where_Clause" />
       <include refid="Update_By_Example_Where_Clause" />
     </if>
     </if>
   </update>
   </update>
-  <update id="updateByPrimaryKeySelective" parameterType="com.tzld.videoVector.model.po.videoVector.deconstruct.DeconstructVectorConfig">
+  <update id="updateByPrimaryKeySelective" parameterType="com.tzld.videoVector.model.po.videoVector.deconstruct.MysqlDeconstructVectorConfig">
     <!--
     <!--
       WARNING - @mbg.generated
       WARNING - @mbg.generated
       This element is automatically generated by MyBatis Generator, do not modify.
       This element is automatically generated by MyBatis Generator, do not modify.
@@ -448,7 +448,7 @@
     </set>
     </set>
     where id = #{id,jdbcType=BIGINT}
     where id = #{id,jdbcType=BIGINT}
   </update>
   </update>
-  <update id="updateByPrimaryKey" parameterType="com.tzld.videoVector.model.po.videoVector.deconstruct.DeconstructVectorConfig">
+  <update id="updateByPrimaryKey" parameterType="com.tzld.videoVector.model.po.videoVector.deconstruct.MysqlDeconstructVectorConfig">
     <!--
     <!--
       WARNING - @mbg.generated
       WARNING - @mbg.generated
       This element is automatically generated by MyBatis Generator, do not modify.
       This element is automatically generated by MyBatis Generator, do not modify.

+ 0 - 2
server/src/main/java/com/tzld/videoVector/Application.java

@@ -1,7 +1,6 @@
 package com.tzld.videoVector;
 package com.tzld.videoVector;
 
 
 import com.tzld.videoVector.interceptor.CrosDomainAllowInterceptor;
 import com.tzld.videoVector.interceptor.CrosDomainAllowInterceptor;
-import org.mybatis.spring.annotation.MapperScan;
 import org.slf4j.Logger;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.slf4j.LoggerFactory;
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.SpringApplication;
@@ -13,7 +12,6 @@ import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
 import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
 import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
 
 
 @SpringBootApplication
 @SpringBootApplication
-@MapperScan("com.tzld.videoVector.dao")
 @ServletComponentScan("com.tzld.videoVector.controller")
 @ServletComponentScan("com.tzld.videoVector.controller")
 @EnableDiscoveryClient
 @EnableDiscoveryClient
 @EnableFeignClients
 @EnableFeignClients

+ 2 - 1
server/src/main/java/com/tzld/videoVector/controller/VideoSearchController.java

@@ -5,6 +5,7 @@ import com.tzld.videoVector.common.base.CommonResponse;
 import com.tzld.videoVector.model.param.DeconstructParam;
 import com.tzld.videoVector.model.param.DeconstructParam;
 import com.tzld.videoVector.model.param.GetDeconstructParam;
 import com.tzld.videoVector.model.param.GetDeconstructParam;
 import com.tzld.videoVector.model.param.MatchTopNVideoParam;
 import com.tzld.videoVector.model.param.MatchTopNVideoParam;
+import com.tzld.videoVector.model.vo.VideoMatchResult;
 import com.tzld.videoVector.service.VideoSearchService;
 import com.tzld.videoVector.service.VideoSearchService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.PostMapping;
@@ -32,7 +33,7 @@ public class VideoSearchController {
     }
     }
 
 
     @PostMapping("/matchTopNVideo")
     @PostMapping("/matchTopNVideo")
-    public CommonResponse<List<Object>> matchTopNVideo(@RequestBody MatchTopNVideoParam param) {
+    public CommonResponse<List<VideoMatchResult>> matchTopNVideo(@RequestBody MatchTopNVideoParam param) {
         return CommonResponse.success(videoSearchService.matchTopNVideo(param));
         return CommonResponse.success(videoSearchService.matchTopNVideo(param));
     }
     }
 }
 }

+ 1 - 4
server/src/main/resources/application.yml

@@ -1,6 +1,6 @@
 spring:
 spring:
   profiles:
   profiles:
-    active: test
+    active: dev
   application:
   application:
     name: video-vector-server
     name: video-vector-server
 
 
@@ -19,10 +19,7 @@ pagehelper:
   helper-dialect: mysql
   helper-dialect: mysql
 
 
 mybatis:
 mybatis:
-  mapper-locations: classpath:mapper/**/*.xml
   configuration:
   configuration:
-    use-generated-keys: true
-    map-underscore-to-camel-case: true
     log-impl: org.apache.ibatis.logging.stdout.StdOutImpl  # 直接输出到控制台(简单调试用)
     log-impl: org.apache.ibatis.logging.stdout.StdOutImpl  # 直接输出到控制台(简单调试用)
 
 
 app:
 app:

+ 73 - 0
server/src/test/java/DataMigrationTest.java

@@ -0,0 +1,73 @@
+import com.tzld.videoVector.Application;
+import com.tzld.videoVector.job.DataMigrationJob;
+import com.xxl.job.core.biz.model.ReturnT;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+
+@SpringBootTest(classes = Application.class)
+@Slf4j
+public class DataMigrationTest {
+
+    @Autowired
+    private DataMigrationJob dataMigrationJob;
+
+    /**
+     * 迁移 deconstruct_vector_config(建议最先执行)
+     */
+    @Test
+    public void testMigrateVectorConfig() {
+        ReturnT<String> result = dataMigrationJob.migrateVectorConfigToPgJob("");
+        log.info("migrateVectorConfigToPgJob 结果: code={}", result.getCode());
+    }
+
+    /**
+     * 迁移 deconstruct_content
+     */
+    @Test
+    public void testMigrateContent() {
+        ReturnT<String> result = dataMigrationJob.migrateContentToPgJob("");
+        log.info("migrateContentToPgJob 结果: code={}", result.getCode());
+    }
+
+    /**
+     * 迁移 deconstruct_content_vector → content_vectors
+     */
+    @Test
+    public void testMigrateContentVector() {
+        ReturnT<String> result = dataMigrationJob.migrateContentVectorToPgJob("");
+        log.info("migrateContentVectorToPgJob 结果: code={}", result.getCode());
+    }
+
+    /**
+     * 迁移 Redis 视频向量 → video_vectors(跳过多点模式)
+     */
+    @Test
+    public void testMigrateRedisVector() {
+        ReturnT<String> result = dataMigrationJob.migrateRedisVectorToPgJob("");
+        log.info("migrateRedisVectorToPgJob 结果: code={}", result.getCode());
+    }
+
+    /**
+     * 一键执行全部迁移(按依赖顺序)
+     */
+    @Test
+    public void testMigrateAll() {
+        log.info("===== 开始全量数据迁移 =====");
+
+        log.info("--- 1. 迁移向量化配置 ---");
+        dataMigrationJob.migrateVectorConfigToPgJob("");
+
+        log.info("--- 2. 迁移解构内容 ---");
+        dataMigrationJob.migrateContentToPgJob("");
+
+        log.info("--- 3. 迁移素材向量 ---");
+        dataMigrationJob.migrateContentVectorToPgJob("");
+
+        log.info("--- 4. 迁移Redis视频向量 ---");
+        dataMigrationJob.migrateRedisVectorToPgJob("");
+
+        log.info("===== 全量数据迁移完成 =====");
+    }
+}