Преглед изворни кода

重构 deconstructPoints: Singapore RDS → 本地脚本 → Redis → 后端读

调用链路彻底简化:
  本地 Python 脚本 (script/sync_decode_to_redis.py)
    ├─ 读 Singapore RDS aigc_topic_decode_task_result
    ├─ 解析 data_content JSON: 灵感点 / 关键点 / 目的点 / 最终选题 / contribution_results
    ├─ 筛选规则: inspiration_final_result/keypoint_final 中 类型='实质' 的点
    │           分词结果中贡献度 >= 0.8 的词 → 实质词
    │           目的点默认所有分词词视为实质
    └─ 写国内阿里云 Redis (key=recall:vid_decode:{vid}, TTL=30天)

  Java 后端 getDeconstructPoints:
    └─ 直接读 Redis,反序列化为 DeconstructPointsVO 返回前端
    (移除: 调外部 aigc API / 调 supply-content API / fallback 兜底)

DeconstructPointsVO 简化结构:
  vid, title, videoUrl, htmlUrl, topic, highValuePoints[]
    each: {id, type, name, essences:[{word,score}]}

容量: 单条 ~1KB, 11151 条总占用 ~10MB, 复用现有 Redis 实例零成本。

vid=63715940 实测跟 weightUrl.html 表格 4 个 ≥0.8 实质值 (0.95/0.85/0.88/0.80) 完全对应。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
刘立冬 пре 1 недеља
родитељ
комит
0cdaa37dc3

+ 38 - 28
core/src/main/java/com/tzld/videoVector/model/vo/recall/DeconstructPointsVO.java

@@ -1,48 +1,58 @@
 package com.tzld.videoVector.model.vo.recall;
 
-import com.alibaba.fastjson.JSONObject;
 import lombok.Data;
 
+import java.util.List;
+
 /**
- * 视频/素材的解构层级返回
+ * 视频解构层级返回 (筛选 实质≥0.8 的高价值点)
  *
- * 由于外部解构 API 返回的 JSON schema 是动态的(选题/灵感点/目的点/关键点
- * 字段名和层级深度可能变化),这里采用透传策略: 直接把原始 result_json 解析为
- * JSONObject 返回给前端,前端用递归树形组件展示,任意叶子节点都可作为查询文本
- * 触发召回。
+ * 数据来源: Singapore RDS aigc_topic_decode_task_result 表
+ *           → 本地 Python 脚本解析筛选 → 写入国内 Redis
+ *           → 后端按 vid 直接读 Redis (key = recall:vid_decode:{vid})
  */
 @Data
 public class DeconstructPointsVO {
 
-    /** 业务侧 ID (videoId / channelContentId) */
-    private Long id;
+    /** 视频ID */
+    private Long vid;
 
-    /** 业务侧字符串ID,与 deconstruct_content.channel_content_id 对齐 */
-    private String channelContentId;
+    /** 视频标题 */
+    private String title;
 
-    /** 解构任务ID */
-    private String taskId;
+    /** 视频地址 */
+    private String videoUrl;
 
-    /** 内容类型: 1长文 2图文 3视频 */
-    private Integer contentType;
+    /** 带权重的可视化页面 URL */
+    private String htmlUrl;
 
-    /** 业务类型: 0选题 1创作 2制作 */
-    private Integer bizType;
+    /** 选题(最终选题.选题) */
+    private String topic;
 
-    /**
-     * 解构状态: 0待处理 1处理中 2成功 3失败
-     */
-    private Integer status;
+    /** 实质≥0.8 的高价值点 */
+    private List<HighValuePoint> highValuePoints;
 
-    /** 状态文字描述,便于前端展示 */
-    private String statusDesc;
+    @Data
+    public static class HighValuePoint {
+        /** 业务侧 ID,例如 inspiration_1 / purpose_1 / kp_xxxxxx */
+        private String id;
 
-    /** 标题(如有) */
-    private String title;
+        /** 类型: 灵感点 / 目的点 / 关键点 */
+        private String type;
+
+        /** 描述名 */
+        private String name;
+
+        /** 描述拆解出的"实质"分词,score>=0.8 */
+        private List<EssenceWord> essences;
+    }
 
-    /** 解构结果原始 JSON 对象,前端用递归 Tree 渲染 */
-    private JSONObject rawResult;
+    @Data
+    public static class EssenceWord {
+        /** 实质词 */
+        private String word;
 
-    /** 失败原因(若有) */
-    private String failureReason;
+        /** 词级贡献度 0~1 */
+        private Double score;
+    }
 }

+ 27 - 64
core/src/main/java/com/tzld/videoVector/service/recall/impl/VectorRecallTestServiceImpl.java

@@ -8,7 +8,6 @@ import com.tzld.videoVector.common.enums.Modality;
 import com.tzld.videoVector.dao.mapper.videoVector.VideoAiUnderstandingMapper;
 import com.tzld.videoVector.dao.mapper.videoVector.deconstruct.DeconstructContentMapper;
 import com.tzld.videoVector.model.entity.VideoDetail;
-import com.tzld.videoVector.model.param.GetDeconstructParam;
 import com.tzld.videoVector.model.param.MatchTopNVideoParam;
 import com.tzld.videoVector.model.param.recall.MatchByTextParam;
 import com.tzld.videoVector.model.param.recall.MatchByVideoIdParam;
@@ -24,6 +23,7 @@ import com.tzld.videoVector.service.VideoSearchService;
 import com.tzld.videoVector.service.recall.VectorRecallTestService;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.StringRedisTemplate;
 import org.springframework.stereotype.Service;
 import org.springframework.util.CollectionUtils;
 import org.springframework.util.StringUtils;
@@ -59,8 +59,18 @@ public class VectorRecallTestServiceImpl implements VectorRecallTestService {
     @Autowired(required = false)
     private VideoAiUnderstandingMapper videoAiUnderstandingMapper;
 
+    @Autowired(required = false)
+    private StringRedisTemplate stringRedisTemplate;
+
     private static final String PLACEHOLDER = "--";
 
+    /**
+     * Redis Key: recall:vid_decode:{vid}
+     * Value: 本地脚本(script/sync_decode_to_redis.py)解析后的瘦身 JSON,
+     *        含 vid/title/videoUrl/htmlUrl/topic/highValuePoints
+     */
+    private static final String REDIS_KEY_DECODE_PREFIX = "recall:vid_decode:";
+
     @Override
     public VideoBasicVO getVideoDetail(Long videoId) {
         if (videoId == null || videoId <= 0L) {
@@ -129,76 +139,29 @@ public class VectorRecallTestServiceImpl implements VectorRecallTestService {
         if (videoId == null || videoId <= 0L) {
             return null;
         }
-        String channelContentId = String.valueOf(videoId);
-
-        // 1. 按 channel_content_id 查 deconstruct_content (用 WithBLOBs 拿 result_json BLOB)
-        DeconstructContent content;
+        if (stringRedisTemplate == null) {
+            log.warn("getDeconstructPoints: stringRedisTemplate 未注入");
+            return null;
+        }
+        String vid = String.valueOf(videoId);
+        String key = REDIS_KEY_DECODE_PREFIX + vid;
+        String json;
         try {
-            DeconstructContentExample example = new DeconstructContentExample();
-            example.createCriteria().andChannelContentIdEqualTo(channelContentId);
-            example.setOrderByClause("id DESC");
-            List<DeconstructContent> list = deconstructContentMapper.selectByExampleWithBLOBs(example);
-            content = list.isEmpty() ? null : list.get(0);
+            json = stringRedisTemplate.opsForValue().get(key);
         } catch (Exception e) {
-            log.error("getDeconstructPoints: query db fail, videoId={}, err={}", videoId, e.getMessage(), e);
+            log.error("getDeconstructPoints: read redis fail, vid={}, err={}", vid, e.getMessage(), e);
             return null;
         }
-
-        if (content == null) {
-            log.info("getDeconstructPoints: deconstruct_content 表无 channel_content_id={} 的记录", channelContentId);
+        if (!StringUtils.hasText(json)) {
+            log.info("getDeconstructPoints: Redis 无 vid={} 的解构记录(脚本未同步或非视频内容)", vid);
             return null;
         }
-
-        DeconstructPointsVO vo = new DeconstructPointsVO();
-        vo.setId(videoId);
-        vo.setChannelContentId(content.getChannelContentId());
-        vo.setTaskId(content.getTaskId());
-        // 用 Number → intValue 兼容 Byte (a909594) / Short (master) PO 字段类型
-        vo.setContentType(content.getContentType() == null ? null : content.getContentType().intValue());
-        vo.setBizType(content.getBizType() == null ? null : content.getBizType().intValue());
-        vo.setStatus(content.getStatus() == null ? null : content.getStatus().intValue());
-        vo.setStatusDesc(getStatusDesc(content.getStatus()));
-        vo.setTitle(content.getTitle());
-        vo.setFailureReason(content.getFailureReason());
-
-        // 2. 优先用本地 result_json (已 WithBLOBs)
-        if (StringUtils.hasText(content.getResultJson())) {
-            try {
-                vo.setRawResult(JSON.parseObject(content.getResultJson()));
-                return vo;
-            } catch (Exception e) {
-                log.warn("getDeconstructPoints: parse local resultJson fail, videoId={}, err={}",
-                        videoId, e.getMessage());
-            }
-        }
-
-        // 3. 本地 result_json 为空 → 用 taskId 调现有 getDeconstructResult,
-        //    它内部会调外部 API 拉数据 + 回填本地表
-        if (StringUtils.hasText(content.getTaskId())) {
-            try {
-                GetDeconstructParam p = new GetDeconstructParam();
-                p.setTaskId(content.getTaskId());
-                p.setBizType(content.getBizType() == null ? null : content.getBizType().intValue());
-                p.setContentType(content.getContentType() == null ? null : content.getContentType().intValue());
-                p.setChannelContentId(channelContentId);
-                JSONObject apiResult = videoSearchService.getDeconstructResult(p);
-                if (apiResult != null) {
-                    // 现有 getDeconstructResult 返回结构: { result: {...}, status, taskId, url }
-                    // 解构 JSON 在 result 字段里, 透传给前端
-                    Object inner = apiResult.get("result");
-                    if (inner instanceof JSONObject) {
-                        vo.setRawResult((JSONObject) inner);
-                    } else if (inner != null) {
-                        // 兜底: 整个 apiResult 透传
-                        vo.setRawResult(apiResult);
-                    }
-                }
-            } catch (Exception e) {
-                log.warn("getDeconstructPoints: fallback to videoSearchService.getDeconstructResult failed, videoId={}, taskId={}, err={}",
-                        videoId, content.getTaskId(), e.getMessage());
-            }
+        try {
+            return JSON.parseObject(json, DeconstructPointsVO.class);
+        } catch (Exception e) {
+            log.error("getDeconstructPoints: parse redis value fail, vid={}, err={}", vid, e.getMessage(), e);
+            return null;
         }
-        return vo;
     }
 
     @Override