|
|
@@ -5,17 +5,32 @@ import com.tzld.videoVector.common.constant.VectorConstants;
|
|
|
import com.tzld.videoVector.dao.mapper.pgVector.DeconstructContentMapper;
|
|
|
import com.tzld.videoVector.dao.mapper.pgVector.DeconstructVectorConfigMapper;
|
|
|
import com.tzld.videoVector.dao.mapper.pgVector.ext.ContentVectorMapperExt;
|
|
|
+import com.tzld.videoVector.dao.mapper.pgVector.ext.MaterialDeconstructResultMapperExt;
|
|
|
+import com.tzld.videoVector.dao.mapper.pgVector.ext.MaterialQualityMapperExt;
|
|
|
+import com.tzld.videoVector.model.entity.ArticleMatch;
|
|
|
+import com.tzld.videoVector.model.entity.MaterialMatch;
|
|
|
+import com.tzld.videoVector.model.entity.VideoMatch;
|
|
|
import com.tzld.videoVector.model.param.MaterialMatchParam;
|
|
|
import com.tzld.videoVector.model.param.MaterialSubmitParam;
|
|
|
+import com.tzld.videoVector.model.param.RecallMaterialScoreParam;
|
|
|
import com.tzld.videoVector.model.po.pgVector.ContentVector;
|
|
|
import com.tzld.videoVector.model.po.pgVector.DeconstructContent;
|
|
|
import com.tzld.videoVector.model.po.pgVector.DeconstructContentExample;
|
|
|
import com.tzld.videoVector.model.po.pgVector.DeconstructVectorConfig;
|
|
|
import com.tzld.videoVector.model.po.pgVector.DeconstructVectorConfigExample;
|
|
|
+import com.tzld.videoVector.model.po.pgVector.MaterialDeconstructResult;
|
|
|
+import com.tzld.videoVector.model.po.pgVector.MaterialQuality;
|
|
|
+import com.tzld.videoVector.model.po.pgVector.MaterialVector;
|
|
|
import com.tzld.videoVector.model.vo.MaterialMatchResult;
|
|
|
+import com.tzld.videoVector.model.vo.MaterialQualityVO;
|
|
|
+import com.tzld.videoVector.model.vo.RecallMaterialScoreVO;
|
|
|
+import com.tzld.videoVector.model.vo.RecallMaterialScoreVO.ScoredMaterial;
|
|
|
+import com.tzld.videoVector.service.ArticleVectorStoreService;
|
|
|
import com.tzld.videoVector.service.DeconstructService;
|
|
|
import com.tzld.videoVector.service.EmbeddingService;
|
|
|
import com.tzld.videoVector.service.MaterialSearchService;
|
|
|
+import com.tzld.videoVector.service.MaterialVectorStoreService;
|
|
|
+import com.tzld.videoVector.service.VectorStoreService;
|
|
|
import com.tzld.videoVector.service.VectorizeService;
|
|
|
import com.tzld.videoVector.util.Md5Util;
|
|
|
import com.tzld.videoVector.util.VectorUtils;
|
|
|
@@ -58,6 +73,26 @@ public class MaterialSearchServiceImpl implements MaterialSearchService {
|
|
|
@Resource
|
|
|
private ContentVectorMapperExt contentVectorMapperExt;
|
|
|
|
|
|
+ @Resource
|
|
|
+ private MaterialVectorStoreService materialVectorStoreService;
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private VectorStoreService vectorStoreService;
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private ArticleVectorStoreService articleVectorStoreService;
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private MaterialQualityMapperExt materialQualityMapperExt;
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private MaterialDeconstructResultMapperExt materialDeconstructResultMapperExt;
|
|
|
+
|
|
|
+ // 默认参数
|
|
|
+ private static final double DEFAULT_ALPHA = 0.7;
|
|
|
+ private static final double DEFAULT_SIM_MIN = 0.7;
|
|
|
+ private static final int DEFAULT_EXPANSION_FACTOR = 3;
|
|
|
+
|
|
|
// ================================================================ 入库
|
|
|
@Override
|
|
|
public String submitMaterial(MaterialSubmitParam param) {
|
|
|
@@ -266,8 +301,448 @@ public class MaterialSearchServiceImpl implements MaterialSearchService {
|
|
|
return result;
|
|
|
}
|
|
|
|
|
|
+ // ================================================================ 质量加权召回
|
|
|
+ @Override
|
|
|
+ public RecallMaterialScoreVO recallMaterialWithQuality(RecallMaterialScoreParam param) {
|
|
|
+ if (param == null) {
|
|
|
+ log.error("recallMaterialWithQuality 参数为空");
|
|
|
+ return RecallMaterialScoreVO.of(Collections.emptyList());
|
|
|
+ }
|
|
|
+
|
|
|
+ int topN = param.getTopN() != null && param.getTopN() > 0 ? param.getTopN() : 10;
|
|
|
+ int expansionFactor = param.getExpansionFactor() != null && param.getExpansionFactor() > 0
|
|
|
+ ? param.getExpansionFactor() : DEFAULT_EXPANSION_FACTOR;
|
|
|
+ double alpha = param.getAlpha() != null ? param.getAlpha() : DEFAULT_ALPHA;
|
|
|
+ double simMin = param.getSimMin() != null ? param.getSimMin() : DEFAULT_SIM_MIN;
|
|
|
+ String configCode = param.getConfigCode();
|
|
|
+ if (!StringUtils.hasText(configCode)) {
|
|
|
+ configCode = DEFAULT_CONFIG_CODE;
|
|
|
+ }
|
|
|
+
|
|
|
+ int candidateSize = topN * expansionFactor;
|
|
|
+ log.info("质量加权召回开始, configCode={}, topN={}, expansionFactor={}, alpha={}, simMin={}, sourceType={}",
|
|
|
+ configCode, topN, expansionFactor, alpha, simMin, param.getSourceType());
|
|
|
+
|
|
|
+ // Step 1: 解析查询向量
|
|
|
+ List<Float> queryVector = resolveQueryVectorForRecall(param);
|
|
|
+ if (queryVector == null || queryVector.isEmpty()) {
|
|
|
+ log.error("质量加权召回:无法获取查询向量");
|
|
|
+ return RecallMaterialScoreVO.of(Collections.emptyList());
|
|
|
+ }
|
|
|
+
|
|
|
+ // Step 2: 跨模态向量召回(视频 + 素材 + 长文),每种模态独立 topN
|
|
|
+ List<ScoredMaterial> allItems = new ArrayList<>();
|
|
|
+
|
|
|
+ // —— 视频 ——
|
|
|
+ List<VideoMatch> videoCandidates = vectorStoreService.searchTopN(configCode, queryVector, candidateSize);
|
|
|
+ if (videoCandidates != null && !videoCandidates.isEmpty()) {
|
|
|
+ allItems.addAll(buildModalityItems(configCode, "VIDEO", videoCandidates, topN, alpha, simMin));
|
|
|
+ log.info("视频召回: 候选 {} 条, 返回", videoCandidates.size());
|
|
|
+ }
|
|
|
+
|
|
|
+ // —— 素材 ——
|
|
|
+ Short sourceType = param.getSourceType();
|
|
|
+ List<MaterialMatch> materialCandidates;
|
|
|
+ if (sourceType != null) {
|
|
|
+ materialCandidates = materialVectorStoreService.searchTopNBySource(configCode, queryVector, candidateSize, sourceType);
|
|
|
+ } else {
|
|
|
+ materialCandidates = materialVectorStoreService.searchTopN(configCode, queryVector, candidateSize);
|
|
|
+ }
|
|
|
+ if (materialCandidates != null && !materialCandidates.isEmpty()) {
|
|
|
+ allItems.addAll(buildMaterialItems(configCode, materialCandidates, topN, alpha, simMin));
|
|
|
+ log.info("素材召回: 候选 {} 条", materialCandidates.size());
|
|
|
+ }
|
|
|
+
|
|
|
+ // —— 长文 ——
|
|
|
+ List<ArticleMatch> articleCandidates = articleVectorStoreService.searchTopN(configCode, queryVector, candidateSize);
|
|
|
+ if (articleCandidates != null && !articleCandidates.isEmpty()) {
|
|
|
+ allItems.addAll(buildModalityItems(configCode, "ARTICLE", articleCandidates, topN, alpha, simMin));
|
|
|
+ log.info("长文召回: 候选 {} 条", articleCandidates.size());
|
|
|
+ }
|
|
|
+
|
|
|
+ log.info("跨模态质量加权召回完成, 共 {} 条", allItems.size());
|
|
|
+ return RecallMaterialScoreVO.of(allItems);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ================================================================ 跨模态 Item 构建
|
|
|
+
|
|
|
+ /** 视频/长文:仅用 sim 评分(质量分暂无) */
|
|
|
+ private <T> List<ScoredMaterial> buildModalityItems(String configCode, String modality,
|
|
|
+ List<T> candidates, int topN,
|
|
|
+ double alpha, double simMin) {
|
|
|
+ List<ScoredMaterial> list = new ArrayList<>();
|
|
|
+ for (T m : candidates) {
|
|
|
+ double sim = getMatchScore(m);
|
|
|
+ if (sim < simMin) continue;
|
|
|
+ ScoredMaterial item = new ScoredMaterial();
|
|
|
+ item.setConfigCode(configCode);
|
|
|
+ item.setModality(modality);
|
|
|
+ item.setSim(round4(sim));
|
|
|
+ item.setQualityScore(0.5);
|
|
|
+ item.setConfidence(0.0);
|
|
|
+ item.setFinalScore(round4(sim)); // 仅用 sim
|
|
|
+ if (m instanceof VideoMatch) {
|
|
|
+ VideoMatch vm = (VideoMatch) m;
|
|
|
+ item.setVideoId(String.valueOf(vm.getVideoId()));
|
|
|
+ item.setMaterialId(item.getVideoId());
|
|
|
+ item.setText(vm.getText());
|
|
|
+ } else if (m instanceof ArticleMatch) {
|
|
|
+ ArticleMatch am = (ArticleMatch) m;
|
|
|
+ item.setArticleId(am.getArticleId());
|
|
|
+ item.setMaterialId(item.getArticleId());
|
|
|
+ item.setText(am.getText());
|
|
|
+ }
|
|
|
+ list.add(item);
|
|
|
+ }
|
|
|
+ list.sort(Comparator.comparingDouble(ScoredMaterial::getFinalScore).reversed());
|
|
|
+ return list.stream().limit(topN).collect(Collectors.toList());
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 素材:sim + quality 复合评分,含完整投放数据和元数据 */
|
|
|
+ private List<ScoredMaterial> buildMaterialItems(String configCode, List<MaterialMatch> candidates,
|
|
|
+ int topN, double alpha, double simMin) {
|
|
|
+ List<String> materialIds = candidates.stream()
|
|
|
+ .map(MaterialMatch::getMaterialId).distinct().collect(Collectors.toList());
|
|
|
+ Map<String, MaterialQuality> qualityMap = batchGetMaterialQuality(materialIds);
|
|
|
+ Map<String, JSONObject> deconstructMap = batchGetMaterialDeconstruct(materialIds);
|
|
|
+
|
|
|
+ List<ScoredMaterial> list = new ArrayList<>();
|
|
|
+ for (MaterialMatch m : candidates) {
|
|
|
+ double sim = m.getScore();
|
|
|
+ if (sim < simMin) continue;
|
|
|
+
|
|
|
+ MaterialQuality mq = qualityMap.get(m.getMaterialId());
|
|
|
+ double qualityScore;
|
|
|
+ double confidence;
|
|
|
+ if (mq != null && mq.getConfidence() != null && mq.getConfidence() > 0.3) {
|
|
|
+ qualityScore = mq.getQualityScore() != null ? mq.getQualityScore() : 0.5;
|
|
|
+ confidence = mq.getConfidence();
|
|
|
+ } else {
|
|
|
+ qualityScore = 0.5;
|
|
|
+ confidence = 0;
|
|
|
+ }
|
|
|
+ double finalScore = alpha * sim + (1 - alpha) * qualityScore;
|
|
|
+
|
|
|
+ ScoredMaterial item = new ScoredMaterial();
|
|
|
+ item.setModality("MATERIAL");
|
|
|
+ item.setMaterialId(m.getMaterialId());
|
|
|
+ item.setConfigCode(configCode);
|
|
|
+ item.setText(m.getText());
|
|
|
+ item.setSim(round4(sim));
|
|
|
+ item.setQualityScore(round4(qualityScore));
|
|
|
+ item.setConfidence(round4(confidence));
|
|
|
+ item.setFinalScore(round4(finalScore));
|
|
|
+
|
|
|
+ // 元数据
|
|
|
+ JSONObject deconstructJson = deconstructMap.get(m.getMaterialId());
|
|
|
+ if (deconstructJson != null) {
|
|
|
+ item.setTitle(extractMaterialTitle(deconstructJson));
|
|
|
+ List<String> images = extractMaterialImages(deconstructJson);
|
|
|
+ if (images != null && !images.isEmpty()) {
|
|
|
+ item.setImageList(images);
|
|
|
+ item.setCover(images.get(0));
|
|
|
+ }
|
|
|
+ item.setDeconstruct(extractDeconstruct(deconstructJson));
|
|
|
+ }
|
|
|
+
|
|
|
+ // 质量数据
|
|
|
+ if (mq != null) {
|
|
|
+ item.setDt(mq.getDt());
|
|
|
+ item.setConversionEfficiencyScore(mq.getConversionEfficiencyScore());
|
|
|
+ item.setRevenueScore(mq.getRevenueScore());
|
|
|
+ item.setViralScore(mq.getViralScore());
|
|
|
+ item.setEngagementScore(mq.getEngagementScore());
|
|
|
+ item.setCost7d(mq.getCost7d());
|
|
|
+ item.setTargetConversion7d(mq.getTargetConversion7d());
|
|
|
+ item.setTotalConversion7d(mq.getTotalConversion7d());
|
|
|
+ item.setRevenue7d(mq.getRevenue7d());
|
|
|
+ item.setT0ViralCount7d(mq.getT0ViralCount7d());
|
|
|
+ item.setT0ViralRate7d(mq.getT0ViralRate7d());
|
|
|
+ item.setMiniProgramOpenRate7d(mq.getMiniProgramOpenRate7d());
|
|
|
+ item.setFirstUv7d(mq.getFirstUv7d());
|
|
|
+ item.setShareCount7d(mq.getShareCount7d());
|
|
|
+ item.setCost30d(mq.getCost30d());
|
|
|
+ item.setTargetConversion30d(mq.getTargetConversion30d());
|
|
|
+ Double cost = mq.getCost7d();
|
|
|
+ if (cost != null && cost > 0) {
|
|
|
+ Long conv = mq.getTargetConversion7d();
|
|
|
+ if (conv != null && conv > 0) item.setCpa7d(round4(cost / conv));
|
|
|
+ Double rev = mq.getRevenue7d();
|
|
|
+ if (rev != null && rev > 0) item.setRoas7d(round4(rev / cost));
|
|
|
+ }
|
|
|
+ item.setAdOptimizationGoal(mq.getAdOptimizationGoal());
|
|
|
+ item.setPackageName(mq.getPackageName());
|
|
|
+ item.setAdStatus(mq.getAdStatus());
|
|
|
+ item.setCreativeStatus(mq.getCreativeStatus());
|
|
|
+ }
|
|
|
+ list.add(item);
|
|
|
+ }
|
|
|
+ list.sort(Comparator.comparingDouble(ScoredMaterial::getFinalScore).reversed());
|
|
|
+ return list.stream().limit(topN).collect(Collectors.toList());
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 从不同类型的 Match 中提取相似度分数 */
|
|
|
+ private double getMatchScore(Object match) {
|
|
|
+ if (match instanceof VideoMatch) return ((VideoMatch) match).getScore();
|
|
|
+ if (match instanceof MaterialMatch) return ((MaterialMatch) match).getScore();
|
|
|
+ if (match instanceof ArticleMatch) return ((ArticleMatch) match).getScore();
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ================================================================ 素材质量查询
|
|
|
+ @Override
|
|
|
+ public MaterialQualityVO getMaterialQuality(String materialId) {
|
|
|
+ if (!StringUtils.hasText(materialId)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ List<MaterialQuality> list = materialQualityMapperExt.selectByMaterialIds(
|
|
|
+ Collections.singletonList(materialId));
|
|
|
+ if (list == null || list.isEmpty()) {
|
|
|
+ log.info("素材质量信息不存在, materialId={}", materialId);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ MaterialQuality mq = list.get(0);
|
|
|
+ MaterialQualityVO vo = new MaterialQualityVO();
|
|
|
+ vo.setMaterialId(mq.getMaterialId());
|
|
|
+ vo.setDt(mq.getDt());
|
|
|
+
|
|
|
+ // 7天投放指标
|
|
|
+ vo.setCost7d(mq.getCost7d());
|
|
|
+ vo.setTargetConversion7d(mq.getTargetConversion7d());
|
|
|
+ vo.setTotalConversion7d(mq.getTotalConversion7d());
|
|
|
+ vo.setRevenue7d(mq.getRevenue7d());
|
|
|
+ vo.setT0ViralCount7d(mq.getT0ViralCount7d());
|
|
|
+ vo.setT0ViralRate7d(mq.getT0ViralRate7d());
|
|
|
+ vo.setMiniProgramOpenRate7d(mq.getMiniProgramOpenRate7d());
|
|
|
+ vo.setFirstUv7d(mq.getFirstUv7d());
|
|
|
+ vo.setShareCount7d(mq.getShareCount7d());
|
|
|
+
|
|
|
+ // 30天兜底
|
|
|
+ vo.setCost30d(mq.getCost30d());
|
|
|
+ vo.setTargetConversion30d(mq.getTargetConversion30d());
|
|
|
+
|
|
|
+ // 效率派生指标
|
|
|
+ Double cost = mq.getCost7d();
|
|
|
+ if (cost != null && cost > 0) {
|
|
|
+ Long conv = mq.getTargetConversion7d();
|
|
|
+ vo.setCpa7d(conv != null && conv > 0 ? round4(cost / conv) : null);
|
|
|
+ Double rev = mq.getRevenue7d();
|
|
|
+ vo.setRoas7d(rev != null && rev > 0 ? round4(rev / cost) : null);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 分维度得分
|
|
|
+ vo.setConversionEfficiencyScore(mq.getConversionEfficiencyScore());
|
|
|
+ vo.setRevenueScore(mq.getRevenueScore());
|
|
|
+ vo.setViralScore(mq.getViralScore());
|
|
|
+ vo.setEngagementScore(mq.getEngagementScore());
|
|
|
+
|
|
|
+ // 综合评分
|
|
|
+ vo.setQualityScore(mq.getQualityScore());
|
|
|
+ vo.setConfidence(mq.getConfidence());
|
|
|
+
|
|
|
+ // 标签
|
|
|
+ vo.setAdOptimizationGoal(mq.getAdOptimizationGoal());
|
|
|
+ vo.setPackageName(mq.getPackageName());
|
|
|
+ vo.setAdStatus(mq.getAdStatus());
|
|
|
+ vo.setCreativeStatus(mq.getCreativeStatus());
|
|
|
+
|
|
|
+ return vo;
|
|
|
+ }
|
|
|
+
|
|
|
// ================================================================ 私有方法
|
|
|
|
|
|
+ /**
|
|
|
+ * 为质量召回解析查询向量
|
|
|
+ * 优先级:materialId > queryVector > queryText
|
|
|
+ */
|
|
|
+ private List<Float> resolveQueryVectorForRecall(RecallMaterialScoreParam param) {
|
|
|
+ String configCode = param.getConfigCode() != null ? param.getConfigCode() : DEFAULT_CONFIG_CODE;
|
|
|
+
|
|
|
+ // 1. 通过 materialId 查该素材的向量,作为 query vector 去搜相似素材
|
|
|
+ if (StringUtils.hasText(param.getMaterialId())) {
|
|
|
+ List<MaterialVector> vectors = materialVectorStoreService.getVectorsByMaterialId(
|
|
|
+ param.getMaterialId(), configCode);
|
|
|
+ if (vectors != null && !vectors.isEmpty()) {
|
|
|
+ String embedding = vectors.get(0).getEmbedding();
|
|
|
+ if (StringUtils.hasText(embedding)) {
|
|
|
+ List<Float> queryVector = VectorUtils.parseVectorString(embedding);
|
|
|
+ if (queryVector != null && !queryVector.isEmpty()) {
|
|
|
+ log.info("使用 materialId={} 的向量作为查询向量, configCode={}, 维度={}",
|
|
|
+ param.getMaterialId(), configCode, queryVector.size());
|
|
|
+ return queryVector;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ log.warn("materialId={} 在 configCode={} 下无向量,回退到其他方式", param.getMaterialId(), configCode);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 直接传入的 queryVector
|
|
|
+ if (param.getQueryVector() != null && !param.getQueryVector().isEmpty()) {
|
|
|
+ return param.getQueryVector();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. queryText embedding
|
|
|
+ if (StringUtils.hasText(param.getQueryText())) {
|
|
|
+ String textHash = Md5Util.encoderByMd5(param.getQueryText());
|
|
|
+ if (StringUtils.hasText(textHash)) {
|
|
|
+ List<Float> cached = materialVectorStoreService.getVectorByTextHash(textHash, configCode);
|
|
|
+ if (cached != null && !cached.isEmpty()) {
|
|
|
+ return cached;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ DeconstructVectorConfig config = getVectorConfigByCode(configCode);
|
|
|
+ return embeddingService.embed(param.getQueryText(), config);
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 批量查询素材解构结果,返回 materialId -> 解析后的 JSON
|
|
|
+ */
|
|
|
+ private Map<String, JSONObject> batchGetMaterialDeconstruct(List<String> materialIds) {
|
|
|
+ if (CollectionUtils.isEmpty(materialIds)) {
|
|
|
+ return Collections.emptyMap();
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ List<MaterialDeconstructResult> rows = materialDeconstructResultMapperExt
|
|
|
+ .selectResultsByMaterialIds("aigc_deconstruct", materialIds);
|
|
|
+ if (rows == null || rows.isEmpty()) {
|
|
|
+ return Collections.emptyMap();
|
|
|
+ }
|
|
|
+ Map<String, JSONObject> map = new HashMap<>();
|
|
|
+ for (MaterialDeconstructResult row : rows) {
|
|
|
+ if (row.getMaterialId() == null || !StringUtils.hasText(row.getResult())) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ JSONObject json = JSONObject.parseObject(row.getResult());
|
|
|
+ map.put(row.getMaterialId(), json);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("解析 material_deconstruct_result JSON 失败, materialId={}", row.getMaterialId());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return map;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("批量查询 material_deconstruct_result 失败: {}", e.getMessage(), e);
|
|
|
+ return Collections.emptyMap();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 从解构 JSON 中提取素材标题 */
|
|
|
+ private String extractMaterialTitle(JSONObject json) {
|
|
|
+ String[] paths = {"target_post.title", "title", "标题", "contentTitle", "素材标题",
|
|
|
+ "input.title", "content.title", "最终选题.name", "最终选题.title"};
|
|
|
+ for (String path : paths) {
|
|
|
+ String val = getJsonPath(json, path);
|
|
|
+ if (StringUtils.hasText(val)) return val;
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 从解构 JSON 中提取素材图片列表 */
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ private List<String> extractMaterialImages(JSONObject json) {
|
|
|
+ String[] paths = {"target_post.images", "target_post.imageList", "images",
|
|
|
+ "imageList", "input.images", "input.imageList"};
|
|
|
+ for (String path : paths) {
|
|
|
+ Object val = getJsonPathValue(json, path);
|
|
|
+ if (val instanceof List) {
|
|
|
+ List<String> list = (List<String>) val;
|
|
|
+ if (!list.isEmpty()) return list;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 按点号分隔路径从 JSON 中取字符串值 */
|
|
|
+ private String getJsonPath(JSONObject json, String path) {
|
|
|
+ String[] keys = path.split("\\.");
|
|
|
+ Object current = json;
|
|
|
+ for (String key : keys) {
|
|
|
+ if (current instanceof JSONObject) {
|
|
|
+ current = ((JSONObject) current).get(key);
|
|
|
+ } else {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return current != null ? current.toString() : null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 从解构 JSON 中提取解构层级(选题 + 灵感点/关键点/目的点) */
|
|
|
+ private Map<String, Object> extractDeconstruct(JSONObject json) {
|
|
|
+ Map<String, Object> result = new HashMap<>();
|
|
|
+ // 选题
|
|
|
+ String topic = extractMaterialTitle(json);
|
|
|
+ if (StringUtils.hasText(topic)) {
|
|
|
+ result.put("topic", topic);
|
|
|
+ }
|
|
|
+ // 灵感点/关键点/目的点 — 提取名称列表
|
|
|
+ extractPointNames(json, result, "灵感点");
|
|
|
+ extractPointNames(json, result, "关键点");
|
|
|
+ extractPointNames(json, result, "目的点");
|
|
|
+ return result.isEmpty() ? null : result;
|
|
|
+ }
|
|
|
+
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ private void extractPointNames(JSONObject json, Map<String, Object> result, String key) {
|
|
|
+ Object val = json.get(key);
|
|
|
+ if (val instanceof com.alibaba.fastjson.JSONArray) {
|
|
|
+ List<String> names = new ArrayList<>();
|
|
|
+ for (Object item : (com.alibaba.fastjson.JSONArray) val) {
|
|
|
+ if (item instanceof JSONObject) {
|
|
|
+ String name = ((JSONObject) item).getString("name");
|
|
|
+ if (name != null) names.add(name);
|
|
|
+ } else if (item instanceof String) {
|
|
|
+ names.add((String) item);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (!names.isEmpty()) result.put(key, names);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private Object getJsonPathValue(JSONObject json, String path) {
|
|
|
+ String[] keys = path.split("\\.");
|
|
|
+ Object current = json;
|
|
|
+ for (String key : keys) {
|
|
|
+ if (current instanceof JSONObject) {
|
|
|
+ current = ((JSONObject) current).get(key);
|
|
|
+ } else {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return current;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 批量查询 material_quality
|
|
|
+ */
|
|
|
+ private Map<String, MaterialQuality> batchGetMaterialQuality(List<String> materialIds) {
|
|
|
+ if (CollectionUtils.isEmpty(materialIds)) {
|
|
|
+ return Collections.emptyMap();
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ List<MaterialQuality> list = materialQualityMapperExt.selectByMaterialIds(materialIds);
|
|
|
+ if (list == null || list.isEmpty()) {
|
|
|
+ return Collections.emptyMap();
|
|
|
+ }
|
|
|
+ Map<String, MaterialQuality> map = new HashMap<>();
|
|
|
+ for (MaterialQuality mq : list) {
|
|
|
+ map.put(mq.getMaterialId(), mq);
|
|
|
+ }
|
|
|
+ return map;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("批量查询 material_quality 失败, ids={}, error={}",
|
|
|
+ materialIds, e.getMessage(), e);
|
|
|
+ return Collections.emptyMap();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static double round4(double v) {
|
|
|
+ return Math.round(v * 10000.0) / 10000.0;
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 解析查询向量
|
|
|
* 优先级:queryVector > channelContentId 历史向量 > queryText embedding
|