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

Merge branch 'feature/luojunhui/20260610-material-stat-improve' of Server/video-vector-server into master

luojunhui пре 9 часа
родитељ
комит
855f50b9e8

+ 21 - 0
core/src/main/java/com/tzld/videoVector/dao/mapper/pgVector/ext/AdsMaterialTouliuAllChannelMapperExt.java

@@ -0,0 +1,21 @@
+package com.tzld.videoVector.dao.mapper.pgVector.ext;
+
+import com.tzld.videoVector.model.po.pgVector.AdsMaterialTouliuAllChannel;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * ads_material_touliu_all_channel 自定义 Mapper
+ */
+public interface AdsMaterialTouliuAllChannelMapperExt {
+
+    /**
+     * 按 materialId 列表聚合近 N 天投放数据
+     * @param materialIds 素材ID列表
+     * @param dtThreshold dt >= 此值(yyyyMMdd),Java 端根据 days 计算
+     */
+    List<AdsMaterialTouliuAllChannel> aggregateByMaterialIds(
+            @Param("materialIds") List<String> materialIds,
+            @Param("dtThreshold") String dtThreshold);
+}

+ 26 - 0
core/src/main/java/com/tzld/videoVector/model/param/RecallMaterialScoreParam.java

@@ -34,6 +34,20 @@ public class RecallMaterialScoreParam {
     /** 最小相似度阈值,低于此值的候选直接过滤 */
     private Double simMin;
 
+    /** 投放数据统计天数,默认 7 */
+    private Integer days;
+
+    /** CTR 权重(默认 0.2) */
+    private Double wCtr;
+    /** CVR 权重(默认 0.2) */
+    private Double wCvr;
+    /** ROI 权重(默认 0.2) */
+    private Double wRoi;
+    /** 小程序打开率权重(默认 0.2) */
+    private Double wOpenRate;
+    /** T0裂变率权重(默认 0.2) */
+    private Double wFissionRate;
+
     public String getMaterialId() { return materialId; }
     public void setMaterialId(String materialId) { this.materialId = materialId; }
     public String getQueryText() { return queryText; }
@@ -52,4 +66,16 @@ public class RecallMaterialScoreParam {
     public void setAlpha(Double alpha) { this.alpha = alpha; }
     public Double getSimMin() { return simMin; }
     public void setSimMin(Double simMin) { this.simMin = simMin; }
+    public Integer getDays() { return days; }
+    public void setDays(Integer days) { this.days = days; }
+    public Double getWCtr() { return wCtr; }
+    public void setWCtr(Double wCtr) { this.wCtr = wCtr; }
+    public Double getWCvr() { return wCvr; }
+    public void setWCvr(Double wCvr) { this.wCvr = wCvr; }
+    public Double getWRoi() { return wRoi; }
+    public void setWRoi(Double wRoi) { this.wRoi = wRoi; }
+    public Double getWOpenRate() { return wOpenRate; }
+    public void setWOpenRate(Double wOpenRate) { this.wOpenRate = wOpenRate; }
+    public Double getWFissionRate() { return wFissionRate; }
+    public void setWFissionRate(Double wFissionRate) { this.wFissionRate = wFissionRate; }
 }

+ 21 - 0
core/src/main/java/com/tzld/videoVector/model/po/pgVector/AdsMaterialTouliuAllChannel.java

@@ -0,0 +1,21 @@
+package com.tzld.videoVector.model.po.pgVector;
+
+import lombok.Data;
+
+/**
+ * ads_material_touliu_all_channel 聚合结果 PO
+ * 按 material_id 聚合近 N 天投放数据
+ */
+@Data
+public class AdsMaterialTouliuAllChannel {
+
+    private String materialId;
+
+    private Long impressions;
+    private Long clicks;
+    private Long conversions;
+    private Double cost;
+    private Double income;
+    private Long firstUv;
+    private Long fission0Uv;
+}

+ 57 - 76
core/src/main/java/com/tzld/videoVector/model/vo/MaterialQualityVO.java

@@ -1,47 +1,38 @@
 package com.tzld.videoVector.model.vo;
 
 /**
- * 素材质量统计信息(enrich 接口返回)
+ * 素材投放质量统计信息
+ * 数据来源:ads_material_touliu_all_channel 近 N 天聚合
  */
 public class MaterialQualityVO {
 
     private String materialId;
-    private String dt;          // 统计截止日期
+    private String dt;              // 统计天数
 
-    // 7天投放指标
-    private Double cost7d;
-    private Long targetConversion7d;
-    private Long totalConversion7d;
-    private Double revenue7d;
-    private Long t0ViralCount7d;
-    private Double t0ViralRate7d;
-    private Double miniProgramOpenRate7d;
-    private Long firstUv7d;
-    private Long shareCount7d;
+    // 效率指标
+    private Double ctr;             // 点击/曝光
+    private Double cvr;             // 转化/曝光
+    private Double roi;             // 收入/成本
+    private Double openRate;        // 首层uv/转化
+    private Double fissionRate;     // T0裂变uv/首层uv
 
-    // 30天兜底指标
-    private Double cost30d;
-    private Long targetConversion30d;
+    // 原始投放数据求和
+    private Long impressions;
+    private Long clicks;
+    private Long conversions;
+    private Double cost;
+    private Double income;
+    private Long firstUv;
+    private Long fission0Uv;
 
-    // 效率派生指标
-    private Double cpa7d;            // 7天单次转化成本 = cost7d / targetConversion7d
-    private Double roas7d;           // 7天ROAS = revenue7d / cost7d
+    // 各维度百分位得分(单素材查询时无批内排名,置 null)
+    private Double ctrScore;
+    private Double cvrScore;
+    private Double roiScore;
+    private Double openRateScore;
+    private Double fissionRateScore;
 
-    // 分维度得分
-    private Double conversionEfficiencyScore;
-    private Double revenueScore;
-    private Double viralScore;
-    private Double engagementScore;
-
-    // 综合评分
     private Double qualityScore;
-    private Double confidence;
-
-    // 标签
-    private String adOptimizationGoal;
-    private String packageName;
-    private String adStatus;
-    private String creativeStatus;
 
     // ===== getters/setters =====
 
@@ -49,50 +40,40 @@ public class MaterialQualityVO {
     public void setMaterialId(String materialId) { this.materialId = materialId; }
     public String getDt() { return dt; }
     public void setDt(String dt) { this.dt = dt; }
-    public Double getCost7d() { return cost7d; }
-    public void setCost7d(Double cost7d) { this.cost7d = cost7d; }
-    public Long getTargetConversion7d() { return targetConversion7d; }
-    public void setTargetConversion7d(Long targetConversion7d) { this.targetConversion7d = targetConversion7d; }
-    public Long getTotalConversion7d() { return totalConversion7d; }
-    public void setTotalConversion7d(Long totalConversion7d) { this.totalConversion7d = totalConversion7d; }
-    public Double getRevenue7d() { return revenue7d; }
-    public void setRevenue7d(Double revenue7d) { this.revenue7d = revenue7d; }
-    public Long getT0ViralCount7d() { return t0ViralCount7d; }
-    public void setT0ViralCount7d(Long t0ViralCount7d) { this.t0ViralCount7d = t0ViralCount7d; }
-    public Double getT0ViralRate7d() { return t0ViralRate7d; }
-    public void setT0ViralRate7d(Double t0ViralRate7d) { this.t0ViralRate7d = t0ViralRate7d; }
-    public Double getMiniProgramOpenRate7d() { return miniProgramOpenRate7d; }
-    public void setMiniProgramOpenRate7d(Double miniProgramOpenRate7d) { this.miniProgramOpenRate7d = miniProgramOpenRate7d; }
-    public Long getFirstUv7d() { return firstUv7d; }
-    public void setFirstUv7d(Long firstUv7d) { this.firstUv7d = firstUv7d; }
-    public Long getShareCount7d() { return shareCount7d; }
-    public void setShareCount7d(Long shareCount7d) { this.shareCount7d = shareCount7d; }
-    public Double getCost30d() { return cost30d; }
-    public void setCost30d(Double cost30d) { this.cost30d = cost30d; }
-    public Long getTargetConversion30d() { return targetConversion30d; }
-    public void setTargetConversion30d(Long targetConversion30d) { this.targetConversion30d = targetConversion30d; }
-    public Double getCpa7d() { return cpa7d; }
-    public void setCpa7d(Double cpa7d) { this.cpa7d = cpa7d; }
-    public Double getRoas7d() { return roas7d; }
-    public void setRoas7d(Double roas7d) { this.roas7d = roas7d; }
-    public Double getConversionEfficiencyScore() { return conversionEfficiencyScore; }
-    public void setConversionEfficiencyScore(Double conversionEfficiencyScore) { this.conversionEfficiencyScore = conversionEfficiencyScore; }
-    public Double getRevenueScore() { return revenueScore; }
-    public void setRevenueScore(Double revenueScore) { this.revenueScore = revenueScore; }
-    public Double getViralScore() { return viralScore; }
-    public void setViralScore(Double viralScore) { this.viralScore = viralScore; }
-    public Double getEngagementScore() { return engagementScore; }
-    public void setEngagementScore(Double engagementScore) { this.engagementScore = engagementScore; }
+    public Double getCtr() { return ctr; }
+    public void setCtr(Double ctr) { this.ctr = ctr; }
+    public Double getCvr() { return cvr; }
+    public void setCvr(Double cvr) { this.cvr = cvr; }
+    public Double getRoi() { return roi; }
+    public void setRoi(Double roi) { this.roi = roi; }
+    public Double getOpenRate() { return openRate; }
+    public void setOpenRate(Double openRate) { this.openRate = openRate; }
+    public Double getFissionRate() { return fissionRate; }
+    public void setFissionRate(Double fissionRate) { this.fissionRate = fissionRate; }
+    public Long getImpressions() { return impressions; }
+    public void setImpressions(Long impressions) { this.impressions = impressions; }
+    public Long getClicks() { return clicks; }
+    public void setClicks(Long clicks) { this.clicks = clicks; }
+    public Long getConversions() { return conversions; }
+    public void setConversions(Long conversions) { this.conversions = conversions; }
+    public Double getCost() { return cost; }
+    public void setCost(Double cost) { this.cost = cost; }
+    public Double getIncome() { return income; }
+    public void setIncome(Double income) { this.income = income; }
+    public Long getFirstUv() { return firstUv; }
+    public void setFirstUv(Long firstUv) { this.firstUv = firstUv; }
+    public Long getFission0Uv() { return fission0Uv; }
+    public void setFission0Uv(Long fission0Uv) { this.fission0Uv = fission0Uv; }
+    public Double getCtrScore() { return ctrScore; }
+    public void setCtrScore(Double ctrScore) { this.ctrScore = ctrScore; }
+    public Double getCvrScore() { return cvrScore; }
+    public void setCvrScore(Double cvrScore) { this.cvrScore = cvrScore; }
+    public Double getRoiScore() { return roiScore; }
+    public void setRoiScore(Double roiScore) { this.roiScore = roiScore; }
+    public Double getOpenRateScore() { return openRateScore; }
+    public void setOpenRateScore(Double openRateScore) { this.openRateScore = openRateScore; }
+    public Double getFissionRateScore() { return fissionRateScore; }
+    public void setFissionRateScore(Double fissionRateScore) { this.fissionRateScore = fissionRateScore; }
     public Double getQualityScore() { return qualityScore; }
     public void setQualityScore(Double qualityScore) { this.qualityScore = qualityScore; }
-    public Double getConfidence() { return confidence; }
-    public void setConfidence(Double confidence) { this.confidence = confidence; }
-    public String getAdOptimizationGoal() { return adOptimizationGoal; }
-    public void setAdOptimizationGoal(String adOptimizationGoal) { this.adOptimizationGoal = adOptimizationGoal; }
-    public String getPackageName() { return packageName; }
-    public void setPackageName(String packageName) { this.packageName = packageName; }
-    public String getAdStatus() { return adStatus; }
-    public void setAdStatus(String adStatus) { this.adStatus = adStatus; }
-    public String getCreativeStatus() { return creativeStatus; }
-    public void setCreativeStatus(String creativeStatus) { this.creativeStatus = creativeStatus; }
 }

+ 54 - 73
core/src/main/java/com/tzld/videoVector/model/vo/RecallMaterialScoreVO.java

@@ -46,39 +46,30 @@ public class RecallMaterialScoreVO {
         // 评分
         private Double sim;             // 余弦相似度 [0, 1]
         private Double qualityScore;    // 素材质量分 [0, 1]
-        private Double confidence;      // 质量分置信度 [0, 1]
         private Double finalScore;      // 综合得分 = alpha*sim + (1-alpha)*qualityScore
 
-        // 质量分维度明细
-        private Double conversionEfficiencyScore;
-        private Double revenueScore;
-        private Double viralScore;
-        private Double engagementScore;
+        // 效率指标(由原始数据求和后计算)
+        private Double ctr;              // 点击/曝光
+        private Double cvr;              // 转化/曝光
+        private Double roi;              // 收入/成本
+        private Double openRate;         // 首层uv/转化(小程序打开率)
+        private Double fissionRate;      // T0裂变uv/首层uv
 
-        // 7天投放指标
-        private Double cost7d;
-        private Long targetConversion7d;
-        private Long totalConversion7d;
-        private Double revenue7d;
-        private Long t0ViralCount7d;
-        private Double t0ViralRate7d;
-        private Double miniProgramOpenRate7d;
-        private Long firstUv7d;
-        private Long shareCount7d;
+        // 原始投放数据求和(近 N 天)
+        private Long impressions;
+        private Long clicks;
+        private Long conversions;
+        private Double cost;
+        private Double income;
+        private Long firstUv;
+        private Long fission0Uv;
 
-        // 30天兜底指标
-        private Double cost30d;
-        private Long targetConversion30d;
-
-        // 效率派生指标
-        private Double cpa7d;            // 7天单次转化成本
-        private Double roas7d;           // 7天ROAS
-
-        // 标签
-        private String adOptimizationGoal;
-        private String packageName;
-        private String adStatus;
-        private String creativeStatus;
+        // 各维度百分位得分
+        private Double ctrScore;
+        private Double cvrScore;
+        private Double roiScore;
+        private Double openRateScore;
+        private Double fissionRateScore;
 
         // ===== getters/setters =====
 
@@ -112,51 +103,41 @@ public class RecallMaterialScoreVO {
         public void setSim(Double sim) { this.sim = sim; }
         public Double getQualityScore() { return qualityScore; }
         public void setQualityScore(Double qualityScore) { this.qualityScore = qualityScore; }
-        public Double getConfidence() { return confidence; }
-        public void setConfidence(Double confidence) { this.confidence = confidence; }
         public Double getFinalScore() { return finalScore; }
         public void setFinalScore(Double finalScore) { this.finalScore = finalScore; }
-        public Double getConversionEfficiencyScore() { return conversionEfficiencyScore; }
-        public void setConversionEfficiencyScore(Double conversionEfficiencyScore) { this.conversionEfficiencyScore = conversionEfficiencyScore; }
-        public Double getRevenueScore() { return revenueScore; }
-        public void setRevenueScore(Double revenueScore) { this.revenueScore = revenueScore; }
-        public Double getViralScore() { return viralScore; }
-        public void setViralScore(Double viralScore) { this.viralScore = viralScore; }
-        public Double getEngagementScore() { return engagementScore; }
-        public void setEngagementScore(Double engagementScore) { this.engagementScore = engagementScore; }
-        public Double getCost7d() { return cost7d; }
-        public void setCost7d(Double cost7d) { this.cost7d = cost7d; }
-        public Long getTargetConversion7d() { return targetConversion7d; }
-        public void setTargetConversion7d(Long targetConversion7d) { this.targetConversion7d = targetConversion7d; }
-        public Long getTotalConversion7d() { return totalConversion7d; }
-        public void setTotalConversion7d(Long totalConversion7d) { this.totalConversion7d = totalConversion7d; }
-        public Double getRevenue7d() { return revenue7d; }
-        public void setRevenue7d(Double revenue7d) { this.revenue7d = revenue7d; }
-        public Long getT0ViralCount7d() { return t0ViralCount7d; }
-        public void setT0ViralCount7d(Long t0ViralCount7d) { this.t0ViralCount7d = t0ViralCount7d; }
-        public Double getT0ViralRate7d() { return t0ViralRate7d; }
-        public void setT0ViralRate7d(Double t0ViralRate7d) { this.t0ViralRate7d = t0ViralRate7d; }
-        public Double getMiniProgramOpenRate7d() { return miniProgramOpenRate7d; }
-        public void setMiniProgramOpenRate7d(Double miniProgramOpenRate7d) { this.miniProgramOpenRate7d = miniProgramOpenRate7d; }
-        public Long getFirstUv7d() { return firstUv7d; }
-        public void setFirstUv7d(Long firstUv7d) { this.firstUv7d = firstUv7d; }
-        public Long getShareCount7d() { return shareCount7d; }
-        public void setShareCount7d(Long shareCount7d) { this.shareCount7d = shareCount7d; }
-        public Double getCost30d() { return cost30d; }
-        public void setCost30d(Double cost30d) { this.cost30d = cost30d; }
-        public Long getTargetConversion30d() { return targetConversion30d; }
-        public void setTargetConversion30d(Long targetConversion30d) { this.targetConversion30d = targetConversion30d; }
-        public Double getCpa7d() { return cpa7d; }
-        public void setCpa7d(Double cpa7d) { this.cpa7d = cpa7d; }
-        public Double getRoas7d() { return roas7d; }
-        public void setRoas7d(Double roas7d) { this.roas7d = roas7d; }
-        public String getAdOptimizationGoal() { return adOptimizationGoal; }
-        public void setAdOptimizationGoal(String adOptimizationGoal) { this.adOptimizationGoal = adOptimizationGoal; }
-        public String getPackageName() { return packageName; }
-        public void setPackageName(String packageName) { this.packageName = packageName; }
-        public String getAdStatus() { return adStatus; }
-        public void setAdStatus(String adStatus) { this.adStatus = adStatus; }
-        public String getCreativeStatus() { return creativeStatus; }
-        public void setCreativeStatus(String creativeStatus) { this.creativeStatus = creativeStatus; }
+        public Double getCtr() { return ctr; }
+        public void setCtr(Double ctr) { this.ctr = ctr; }
+        public Double getCvr() { return cvr; }
+        public void setCvr(Double cvr) { this.cvr = cvr; }
+        public Double getRoi() { return roi; }
+        public void setRoi(Double roi) { this.roi = roi; }
+        public Double getOpenRate() { return openRate; }
+        public void setOpenRate(Double openRate) { this.openRate = openRate; }
+        public Double getFissionRate() { return fissionRate; }
+        public void setFissionRate(Double fissionRate) { this.fissionRate = fissionRate; }
+        public Long getImpressions() { return impressions; }
+        public void setImpressions(Long impressions) { this.impressions = impressions; }
+        public Long getClicks() { return clicks; }
+        public void setClicks(Long clicks) { this.clicks = clicks; }
+        public Long getConversions() { return conversions; }
+        public void setConversions(Long conversions) { this.conversions = conversions; }
+        public Double getCost() { return cost; }
+        public void setCost(Double cost) { this.cost = cost; }
+        public Double getIncome() { return income; }
+        public void setIncome(Double income) { this.income = income; }
+        public Long getFirstUv() { return firstUv; }
+        public void setFirstUv(Long firstUv) { this.firstUv = firstUv; }
+        public Long getFission0Uv() { return fission0Uv; }
+        public void setFission0Uv(Long fission0Uv) { this.fission0Uv = fission0Uv; }
+        public Double getCtrScore() { return ctrScore; }
+        public void setCtrScore(Double ctrScore) { this.ctrScore = ctrScore; }
+        public Double getCvrScore() { return cvrScore; }
+        public void setCvrScore(Double cvrScore) { this.cvrScore = cvrScore; }
+        public Double getRoiScore() { return roiScore; }
+        public void setRoiScore(Double roiScore) { this.roiScore = roiScore; }
+        public Double getOpenRateScore() { return openRateScore; }
+        public void setOpenRateScore(Double openRateScore) { this.openRateScore = openRateScore; }
+        public Double getFissionRateScore() { return fissionRateScore; }
+        public void setFissionRateScore(Double fissionRateScore) { this.fissionRateScore = fissionRateScore; }
     }
 }

+ 117 - 111
core/src/main/java/com/tzld/videoVector/service/impl/MaterialSearchServiceImpl.java

@@ -4,21 +4,21 @@ import com.alibaba.fastjson.JSONObject;
 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.AdsMaterialTouliuAllChannelMapperExt;
 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.AdsMaterialTouliuAllChannel;
 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.service.rank.RankingParams;
 import com.tzld.videoVector.model.vo.MaterialMatchResult;
@@ -32,6 +32,7 @@ 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.MaterialQualityCalculator;
 import com.tzld.videoVector.util.Md5Util;
 import com.tzld.videoVector.util.VectorUtils;
 import lombok.extern.slf4j.Slf4j;
@@ -40,6 +41,8 @@ import org.springframework.util.CollectionUtils;
 import org.springframework.util.StringUtils;
 
 import javax.annotation.Resource;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
 import java.util.*;
 import java.util.stream.Collectors;
 
@@ -83,7 +86,7 @@ public class MaterialSearchServiceImpl implements MaterialSearchService {
     private ArticleVectorStoreService articleVectorStoreService;
 
     @Resource
-    private MaterialQualityMapperExt materialQualityMapperExt;
+    private AdsMaterialTouliuAllChannelMapperExt adsMaterialTouliuAllChannelMapperExt;
 
     @Resource
     private MaterialDeconstructResultMapperExt materialDeconstructResultMapperExt;
@@ -317,8 +320,16 @@ public class MaterialSearchServiceImpl implements MaterialSearchService {
         }
 
         int candidateSize = topN * expansionFactor;
-        log.info("质量加权召回开始, configCode={}, topN={}, expansionFactor={}, alpha={}, simMin={}, sourceType={}",
-                configCode, topN, expansionFactor, alpha, simMin, param.getSourceType());
+        int days = param.getDays() != null ? param.getDays() : 7;
+        double wCtr = param.getWCtr() != null ? param.getWCtr() : 0.2;
+        double wCvr = param.getWCvr() != null ? param.getWCvr() : 0.2;
+        double wRoi = param.getWRoi() != null ? param.getWRoi() : 0.2;
+        double wOpenRate = param.getWOpenRate() != null ? param.getWOpenRate() : 0.2;
+        double wFissionRate = param.getWFissionRate() != null ? param.getWFissionRate() : 0.2;
+        log.info("质量加权召回开始, configCode={}, topN={}, expansionFactor={}, alpha={}, simMin={}, sourceType={}, days={}," +
+                        " wCtr={}, wCvr={}, wRoi={}, wOpenRate={}, wFissionRate={}",
+                configCode, topN, expansionFactor, alpha, simMin, param.getSourceType(),
+                days, wCtr, wCvr, wRoi, wOpenRate, wFissionRate);
 
         // Step 1: 解析查询向量
         List<Float> queryVector = resolveQueryVectorForRecall(param);
@@ -346,7 +357,8 @@ public class MaterialSearchServiceImpl implements MaterialSearchService {
             materialCandidates = materialVectorStoreService.searchTopN(configCode, queryVector, candidateSize);
         }
         if (materialCandidates != null && !materialCandidates.isEmpty()) {
-            allItems.addAll(buildMaterialItems(configCode, materialCandidates, topN, alpha, simMin));
+            allItems.addAll(buildMaterialItems(configCode, materialCandidates, topN, alpha, simMin,
+                    days, wCtr, wCvr, wRoi, wOpenRate, wFissionRate));
             log.info("素材召回: 候选 {} 条", materialCandidates.size());
         }
 
@@ -376,10 +388,9 @@ public class MaterialSearchServiceImpl implements MaterialSearchService {
             ScoredMaterial item = new ScoredMaterial();
             item.setConfigCode(configCode);
             item.setModality(modality);
-            item.setSim(round4(sim));
+            item.setSim(MaterialQualityCalculator.round4(sim));
             item.setQualityScore(0.5);
-            item.setConfidence(0.0);
-            item.setFinalScore(round4(simNorm));
+            item.setFinalScore(MaterialQualityCalculator.round4(simNorm));
             if (m instanceof VideoMatch) {
                 VideoMatch vm = (VideoMatch) m;
                 item.setVideoId(String.valueOf(vm.getVideoId()));
@@ -397,29 +408,49 @@ public class MaterialSearchServiceImpl implements MaterialSearchService {
         return list.stream().limit(topN).collect(Collectors.toList());
     }
 
-    /** 素材:sim + quality 复合评分,含完整投放数据和元数据 */
+    /** 素材:sim + quality 复合评分,投放数据从 ads_material_touliu_all_channel 实时聚合,用原始比率直接作为质量分 */
     private List<ScoredMaterial> buildMaterialItems(String configCode, List<MaterialMatch> candidates,
-                                                     int topN, double alpha, double simMin) {
+                                                     int topN, double alpha, double simMin,
+                                                     int days, double wCtr, double wCvr, double wRoi,
+                                                     double wOpenRate, double wFissionRate) {
         List<String> materialIds = candidates.stream()
                 .map(MaterialMatch::getMaterialId).distinct().collect(Collectors.toList());
-        Map<String, MaterialQuality> qualityMap = batchGetMaterialQuality(materialIds);
+        Map<String, AdsMaterialTouliuAllChannel> statsMap = batchGetMaterialTouliuStats(materialIds, days);
         Map<String, JSONObject> deconstructMap = batchGetMaterialDeconstruct(materialIds);
 
+        // 计算有数据素材的效率指标,直接作为质量分维度(原始比率)
+        Map<String, double[]> effMap = new LinkedHashMap<>(); // materialId -> [ctr, cvr, roi, openRate, fissionRate]
+        for (String mid : materialIds) {
+            AdsMaterialTouliuAllChannel s = statsMap.get(mid);
+            if (s == null) continue;
+            Double ctr = MaterialQualityCalculator.divide(s.getClicks(), s.getImpressions());
+            Double cvr = MaterialQualityCalculator.divide(s.getConversions(), s.getImpressions());
+            Double roi = MaterialQualityCalculator.divide(s.getIncome(), s.getCost());
+            Double openRate = MaterialQualityCalculator.divide(s.getFirstUv(), s.getConversions());
+            Double fissionRate = MaterialQualityCalculator.divide(s.getFission0Uv(), s.getFirstUv());
+            if (ctr != null || cvr != null || roi != null || openRate != null || fissionRate != null) {
+                effMap.put(mid, new double[]{
+                        ctr != null ? ctr : 0, cvr != null ? cvr : 0,
+                        roi != null ? roi : 0, openRate != null ? openRate : 0,
+                        fissionRate != null ? fissionRate : 0});
+            }
+        }
+
         double denom = 1.0 - simMin;
         List<ScoredMaterial> list = new ArrayList<>();
         for (MaterialMatch m : candidates) {
             double sim = m.getScore();
             if (sim < simMin) continue;
 
-            MaterialQuality mq = qualityMap.get(m.getMaterialId());
+            AdsMaterialTouliuAllChannel s = statsMap.get(m.getMaterialId());
+            double[] eff = effMap.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();
+            if (eff != null) {
+                qualityScore = wCtr * eff[0] + wCvr * eff[1] + wRoi * eff[2]
+                             + wOpenRate * eff[3] + wFissionRate * eff[4];
             } else {
-                qualityScore = 0.5;
-                confidence = 0;
+                qualityScore = 0;
             }
             double simNorm = denom > 0 ? clip01((sim - simMin) / denom) : 0;
             double finalScore = alpha * simNorm + (1 - alpha) * qualityScore;
@@ -431,10 +462,9 @@ public class MaterialSearchServiceImpl implements MaterialSearchService {
             item.setSourceType(m.getSourceType());
             item.setSource(mapSourceTypeLabel(m.getSourceType()));
             item.setText(m.getText());
-            item.setSim(round4(sim));
-            item.setQualityScore(round4(qualityScore));
-            item.setConfidence(round4(confidence));
-            item.setFinalScore(round4(finalScore));
+            item.setSim(MaterialQualityCalculator.round4(sim));
+            item.setQualityScore(MaterialQualityCalculator.round4(qualityScore));
+            item.setFinalScore(MaterialQualityCalculator.round4(finalScore));
 
             // 元数据
             JSONObject deconstructJson = deconstructMap.get(m.getMaterialId());
@@ -448,35 +478,29 @@ public class MaterialSearchServiceImpl implements MaterialSearchService {
                 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());
+            // 投放数据填充
+            if (s != null) {
+                item.setDt(String.valueOf(days));
+                item.setImpressions(s.getImpressions());
+                item.setClicks(s.getClicks());
+                item.setConversions(s.getConversions());
+                item.setCost(MaterialQualityCalculator.round4(s.getCost()));
+                item.setIncome(MaterialQualityCalculator.round4(s.getIncome()));
+                item.setFirstUv(s.getFirstUv());
+                item.setFission0Uv(s.getFission0Uv());
+            }
+            if (eff != null) {
+                item.setCtr(MaterialQualityCalculator.round4(eff[0]));
+                item.setCvr(MaterialQualityCalculator.round4(eff[1]));
+                item.setRoi(MaterialQualityCalculator.round4(eff[2]));
+                item.setOpenRate(MaterialQualityCalculator.round4(eff[3]));
+                item.setFissionRate(MaterialQualityCalculator.round4(eff[4]));
+                // 维度得分 = 原始比率
+                item.setCtrScore(MaterialQualityCalculator.round4(eff[0]));
+                item.setCvrScore(MaterialQualityCalculator.round4(eff[1]));
+                item.setRoiScore(MaterialQualityCalculator.round4(eff[2]));
+                item.setOpenRateScore(MaterialQualityCalculator.round4(eff[3]));
+                item.setFissionRateScore(MaterialQualityCalculator.round4(eff[4]));
             }
             list.add(item);
         }
@@ -499,58 +523,40 @@ public class MaterialSearchServiceImpl implements MaterialSearchService {
             return null;
         }
 
-        List<MaterialQuality> list = materialQualityMapperExt.selectByMaterialIds(
-                Collections.singletonList(materialId));
-        if (list == null || list.isEmpty()) {
+        int days = 7;
+        Map<String, AdsMaterialTouliuAllChannel> statsMap = batchGetMaterialTouliuStats(
+                Collections.singletonList(materialId), days);
+        AdsMaterialTouliuAllChannel s = statsMap.get(materialId);
+        if (s == null) {
             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());
-
+        vo.setMaterialId(s.getMaterialId());
+        vo.setDt(String.valueOf(days));
+
+        vo.setImpressions(s.getImpressions());
+        vo.setClicks(s.getClicks());
+        vo.setConversions(s.getConversions());
+        vo.setCost(MaterialQualityCalculator.round4(s.getCost()));
+        vo.setIncome(MaterialQualityCalculator.round4(s.getIncome()));
+        vo.setFirstUv(s.getFirstUv());
+        vo.setFission0Uv(s.getFission0Uv());
+
+        // 效率指标
+        Double ctr = MaterialQualityCalculator.divide(s.getClicks(), s.getImpressions());
+        Double cvr = MaterialQualityCalculator.divide(s.getConversions(), s.getImpressions());
+        Double roi = MaterialQualityCalculator.divide(s.getIncome(), s.getCost());
+        Double openRate = MaterialQualityCalculator.divide(s.getFirstUv(), s.getConversions());
+        Double fissionRate = MaterialQualityCalculator.divide(s.getFission0Uv(), s.getFirstUv());
+        vo.setCtr(ctr != null ? MaterialQualityCalculator.round4(ctr) : null);
+        vo.setCvr(cvr != null ? MaterialQualityCalculator.round4(cvr) : null);
+        vo.setRoi(roi != null ? MaterialQualityCalculator.round4(roi) : null);
+        vo.setOpenRate(openRate != null ? MaterialQualityCalculator.round4(openRate) : null);
+        vo.setFissionRate(fissionRate != null ? MaterialQualityCalculator.round4(fissionRate) : null);
+
+        // 单素材查询无批内排名,维度得分置 null
         return vo;
     }
 
@@ -718,25 +724,29 @@ public class MaterialSearchServiceImpl implements MaterialSearchService {
     }
 
     /**
-     * 批量查询 material_quality
+     * 批量查询 ads_material_touliu_all_channel 聚合数据
      */
-    private Map<String, MaterialQuality> batchGetMaterialQuality(List<String> materialIds) {
+    private Map<String, AdsMaterialTouliuAllChannel> batchGetMaterialTouliuStats(
+            List<String> materialIds, int days) {
         if (CollectionUtils.isEmpty(materialIds)) {
             return Collections.emptyMap();
         }
         try {
-            List<MaterialQuality> list = materialQualityMapperExt.selectByMaterialIds(materialIds);
+            String dtThreshold = LocalDate.now().minusDays(days)
+                    .format(DateTimeFormatter.ofPattern("yyyyMMdd"));
+            List<AdsMaterialTouliuAllChannel> list = adsMaterialTouliuAllChannelMapperExt
+                    .aggregateByMaterialIds(materialIds, dtThreshold);
             if (list == null || list.isEmpty()) {
                 return Collections.emptyMap();
             }
-            Map<String, MaterialQuality> map = new HashMap<>();
-            for (MaterialQuality mq : list) {
-                map.put(mq.getMaterialId(), mq);
+            Map<String, AdsMaterialTouliuAllChannel> map = new HashMap<>();
+            for (AdsMaterialTouliuAllChannel s : list) {
+                map.put(s.getMaterialId(), s);
             }
             return map;
         } catch (Exception e) {
-            log.error("批量查询 material_quality 失败, ids={}, error={}",
-                    materialIds, e.getMessage(), e);
+            log.error("批量查询 ads_material_touliu_all_channel 失败, ids={}, days={}, error={}",
+                    materialIds, days, e.getMessage(), e);
             return Collections.emptyMap();
         }
     }
@@ -745,10 +755,6 @@ public class MaterialSearchServiceImpl implements MaterialSearchService {
         return Math.max(0, Math.min(1, x));
     }
 
-    private static double round4(double v) {
-        return Math.round(v * 10000.0) / 10000.0;
-    }
-
     private static String mapSourceTypeLabel(Short sourceType) {
         if (sourceType == null) return null;
         if (sourceType == 1) return "外部合作";

+ 2 - 2
core/src/main/java/com/tzld/videoVector/service/rank/RankingParams.java

@@ -16,9 +16,9 @@ import java.util.Map;
  *
  * 公式(MATERIAL):
  *   sim_norm = clip((sim - simThreshold) / (1 - simThreshold), 0, 1)
- *   qualityScore = (wCtr × ctr + wViral × viral + wRoi × roi) / qualTotalW
+ *   qualityScore = wCtr×ctr + wCvr×cvr + wRoi×roi + wOpenRate×openRate + wFissionRate×fissionRate
  *   composite = alpha × sim_norm + (1 - alpha) × qualityScore
- *   materialMissingStrategy 控制无质量数据素材的处理策略。
+ *   各维度直接用原始效率比率(非百分位),无投放数据素材 qualityScore = 0
  */
 @Data
 public class RankingParams {

+ 108 - 81
core/src/main/java/com/tzld/videoVector/service/recall/impl/VectorRecallTestServiceImpl.java

@@ -9,10 +9,10 @@ import com.tzld.videoVector.api.VideoApiService;
 import com.tzld.videoVector.common.constant.VectorConstants;
 import com.tzld.videoVector.common.enums.Modality;
 import com.tzld.videoVector.dao.mapper.pgVector.DeconstructVectorConfigMapper;
+import com.tzld.videoVector.dao.mapper.pgVector.ext.AdsMaterialTouliuAllChannelMapperExt;
 import com.tzld.videoVector.dao.mapper.pgVector.ext.ArticleDeconstructResultMapperExt;
 import com.tzld.videoVector.dao.mapper.pgVector.ext.ArticleQualityMapperExt;
 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.VideoDetail;
@@ -27,8 +27,8 @@ import com.tzld.videoVector.model.po.pgVector.ArticleDeconstructResult;
 import com.tzld.videoVector.model.po.pgVector.ArticleQuality;
 import com.tzld.videoVector.model.po.pgVector.DeconstructVectorConfig;
 import com.tzld.videoVector.model.po.pgVector.DeconstructVectorConfigExample;
+import com.tzld.videoVector.model.po.pgVector.AdsMaterialTouliuAllChannel;
 import com.tzld.videoVector.model.po.pgVector.MaterialDeconstructResult;
-import com.tzld.videoVector.model.po.pgVector.MaterialQuality;
 import com.tzld.videoVector.model.vo.VideoMatchResult;
 import com.tzld.videoVector.model.vo.recall.AIUnderstandingVO;
 import com.tzld.videoVector.model.vo.recall.ArticleBasicVO;
@@ -49,6 +49,7 @@ import com.tzld.videoVector.service.MaterialVectorStoreService;
 import com.tzld.videoVector.service.VectorStoreService;
 import com.tzld.videoVector.service.VideoSearchService;
 import com.tzld.videoVector.service.recall.VectorRecallTestService;
+import com.tzld.videoVector.util.MaterialQualityCalculator;
 import com.tzld.videoVector.util.Md5Util;
 import com.tzld.videoVector.util.RedisUtils;
 import com.tzld.videoVector.model.po.pgVector.ArticleVector;
@@ -63,6 +64,8 @@ import org.springframework.util.StringUtils;
 
 import javax.annotation.PreDestroy;
 import javax.annotation.Resource;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
@@ -106,7 +109,7 @@ public class VectorRecallTestServiceImpl implements VectorRecallTestService {
     private MaterialDeconstructResultMapperExt materialDeconstructResultMapperExt;
 
     @Autowired
-    private MaterialQualityMapperExt materialQualityMapperExt;
+    private AdsMaterialTouliuAllChannelMapperExt adsMaterialTouliuAllChannelMapperExt;
 
     @Autowired
     private ArticleVectorStoreService articleVectorStoreService;
@@ -257,10 +260,11 @@ public class VectorRecallTestServiceImpl implements VectorRecallTestService {
         }, RECALL_EXECUTOR)
                 : CompletableFuture.completedFuture(Collections.emptyList());
 
+        int materialDays = param.getDays() != null ? param.getDays() : metricsDays;
         CompletableFuture<List<VideoMatchEnrichedVO>> materialFuture = needMaterial
                 ? CompletableFuture.supplyAsync(
                 () -> recallMaterialItems(param.getQueryText(), finalConfigCode, finalMaterialRecallK,
-                        toSourceType(param.getSourceLabels())),
+                        toSourceType(param.getSourceLabels()), materialDays),
                 RECALL_EXECUTOR)
                 : CompletableFuture.completedFuture(Collections.emptyList());
 
@@ -429,25 +433,25 @@ public class VectorRecallTestServiceImpl implements VectorRecallTestService {
         // 视频去重
         List<VideoMatchResult> dedupedVideo = deduplicateRaw(allRawVideo,
                 VideoMatchResult::getVideoId, m -> m.getScore() != null ? m.getScore() : 0.0);
-        // 素材去重 + 批量查 quality 做轻量预打分(不等 full enrich)
+        // 素材去重 + 批量查 touliu 做轻量预打分(不等 full enrich)
         List<MaterialMatch> dedupedMaterial = deduplicateRaw(allRawMaterial,
                 MaterialMatch::getMaterialId, MaterialMatch::getScore);
         List<String> matIds = dedupedMaterial.stream()
                 .map(MaterialMatch::getMaterialId).distinct().limit(enrichK * 2)
                 .collect(Collectors.toList());
-        Map<String, MaterialQuality> qualitySnapshot = matIds.isEmpty()
-                ? Collections.emptyMap() : loadMaterialQualityRows(matIds);
+        int preDays = param.getDays() != null ? param.getDays() : metricsDays;
+        Map<String, AdsMaterialTouliuAllChannel> statsSnapshot = matIds.isEmpty()
+                ? Collections.emptyMap() : loadMaterialTouliuStats(matIds, preDays);
         // 文章去重
         List<ArticleMatch> dedupedArticle = deduplicateRaw(allRawArticle,
                 ArticleMatch::getArticleId, ArticleMatch::getScore);
 
         double preAlpha = RankingParams.defaults().getAlpha(); // 0.6
 
-        // 素材预打分:sim + quality 轻量复合,有质量数据的获得加成
+        // 素材预打分:sim + quality 轻量复合
         List<MaterialMatch> topMaterial = dedupedMaterial.stream()
                 .sorted(Comparator.comparingDouble((MaterialMatch m) -> {
-                    MaterialQuality mq = qualitySnapshot.get(m.getMaterialId());
-                    double qs = (mq != null && mq.getQualityScore() != null) ? mq.getQualityScore() : 0;
+                    double qs = computeSimpleQualityScore(statsSnapshot.get(m.getMaterialId()));
                     return -(preAlpha * m.getScore() + (1 - preAlpha) * qs);
                 }))
                 .limit(enrichK).collect(Collectors.toList());
@@ -465,10 +469,37 @@ public class VectorRecallTestServiceImpl implements VectorRecallTestService {
         List<VideoMatchEnrichedVO> videoItems = enrichVideoMatches(topVideo, configCodes.get(0), param.getDays());
         for (int i = 0; i < videoItems.size() && i < topVideo.size(); i++) {
             String cc = topVideo.get(i).getConfigCode();
-            if (cc != null) videoItems.get(i).setConfigCode(cc);
+            if (cc != null) {
+                videoItems.get(i).setConfigCode(cc);
+                if (videoItems.get(i).getSignals() != null
+                        && videoItems.get(i).getSignals().getProvenance() != null) {
+                    videoItems.get(i).getSignals().getProvenance().setConfigCode(cc);
+                }
+            }
+        }
+        int matDays = param.getDays() != null ? param.getDays() : metricsDays;
+        List<VideoMatchEnrichedVO> materialItems = enrichMaterialMatches(topMaterial, configCodes.get(0), matDays);
+        for (int i = 0; i < materialItems.size() && i < topMaterial.size(); i++) {
+            String cc = topMaterial.get(i).getConfigCode();
+            if (cc != null) {
+                materialItems.get(i).setConfigCode(cc);
+                if (materialItems.get(i).getSignals() != null
+                        && materialItems.get(i).getSignals().getProvenance() != null) {
+                    materialItems.get(i).getSignals().getProvenance().setConfigCode(cc);
+                }
+            }
         }
-        List<VideoMatchEnrichedVO> materialItems = enrichMaterialMatches(topMaterial, configCodes.get(0));
         List<VideoMatchEnrichedVO> articleItems = enrichArticleMatches(topArticle, configCodes.get(0));
+        for (int i = 0; i < articleItems.size() && i < topArticle.size(); i++) {
+            String cc = topArticle.get(i).getConfigCode();
+            if (cc != null) {
+                articleItems.get(i).setConfigCode(cc);
+                if (articleItems.get(i).getSignals() != null
+                        && articleItems.get(i).getSignals().getProvenance() != null) {
+                    articleItems.get(i).getSignals().getProvenance().setConfigCode(cc);
+                }
+            }
+        }
 
         log.info("batchByText 阶段二 enrich 完成(含预打分): video={}/{}, material={}/{}, article={}/{}",
                 videoItems.size(), dedupedVideo.size(),
@@ -651,7 +682,7 @@ public class VectorRecallTestServiceImpl implements VectorRecallTestService {
      * 素材文本召回:material_vectors → material_deconstruct_result
      */
     private List<VideoMatchEnrichedVO> recallMaterialItems(String queryText, String configCode,
-                                                            int topN, Short sourceType) {
+                                                            int topN, Short sourceType, int days) {
         try {
             int candidate = Math.max(topN * VectorConstants.MULTI_POINT_RECALL_CANDIDATE_FACTOR,
                     VectorConstants.MULTI_POINT_RECALL_MIN_CANDIDATES);
@@ -677,7 +708,7 @@ public class VectorRecallTestServiceImpl implements VectorRecallTestService {
                         }
                         log.info("素材召回(rawVector) 去重后({}条): {}, configCode={}",
                                 matches.size(), matchSample, configCode);
-                        return limitEnrichedItemsByScore(enrichMaterialMatches(matches, configCode), topN);
+                        return limitEnrichedItemsByScore(enrichMaterialMatches(matches, configCode, days), topN);
                     }
                     log.info("素材召回(rawVector) 无结果, configCode={}", configCode);
                     return Collections.emptyList();
@@ -700,7 +731,7 @@ public class VectorRecallTestServiceImpl implements VectorRecallTestService {
                         }
                         log.info("素材召回(parsed vector缓存) 去重后({}条): {}, configCode={}",
                                 matches.size(), matchSample, configCode);
-                        return limitEnrichedItemsByScore(enrichMaterialMatches(matches, configCode), topN);
+                        return limitEnrichedItemsByScore(enrichMaterialMatches(matches, configCode, days), topN);
                     }
                     log.info("素材召回(parsed vector缓存) 无结果, configCode={}", configCode);
                     return Collections.emptyList();
@@ -727,7 +758,7 @@ public class VectorRecallTestServiceImpl implements VectorRecallTestService {
                 matchSample.add(m.getMaterialId() + ":" + String.format("%.4f", m.getScore()));
             }
             log.info("素材召回(embedding API) 去重后({}条): {}, configCode={}", matches.size(), matchSample, configCode);
-            return limitEnrichedItemsByScore(enrichMaterialMatches(matches, configCode), topN);
+            return limitEnrichedItemsByScore(enrichMaterialMatches(matches, configCode, days), topN);
         } catch (Exception e) {
             log.error("素材召回 material_vectors 异常: {}", e.getMessage(), e);
             return Collections.emptyList();
@@ -901,7 +932,7 @@ public class VectorRecallTestServiceImpl implements VectorRecallTestService {
      * - materialDetail.deconstruct:解析 dataContent 得到 topic + 灵感点/关键点/目的点
      * - materialDetail.source:source_type → 中文标签
      */
-    private List<VideoMatchEnrichedVO> enrichMaterialMatches(List<MaterialMatch> matches, String requestConfigCode) {
+    private List<VideoMatchEnrichedVO> enrichMaterialMatches(List<MaterialMatch> matches, String requestConfigCode, int days) {
         if (CollectionUtils.isEmpty(matches)) {
             return Collections.emptyList();
         }
@@ -910,7 +941,7 @@ public class VectorRecallTestServiceImpl implements VectorRecallTestService {
                 .filter(java.util.Objects::nonNull)
                 .collect(Collectors.toList());
         Map<String, MaterialDeconstructResult> rowByMaterialId = loadMaterialDeconstructRows(materialIds);
-        Map<String, MaterialQuality> qualityByMaterialId = loadMaterialQualityRows(materialIds);
+        Map<String, AdsMaterialTouliuAllChannel> statsByMaterialId = loadMaterialTouliuStats(materialIds, days);
 
         List<VideoMatchEnrichedVO> items = new ArrayList<>(matches.size());
         for (MaterialMatch m : matches) {
@@ -941,9 +972,9 @@ public class VectorRecallTestServiceImpl implements VectorRecallTestService {
             fillMaterialDetailImageCount(detail, vo.getImageList());
 
             // 填充投放质量数据
-            MaterialQuality mq = qualityByMaterialId.get(m.getMaterialId());
-            if (mq != null) {
-                detail.setQuality(buildQualityMap(mq));
+            AdsMaterialTouliuAllChannel stats = statsByMaterialId.get(m.getMaterialId());
+            if (stats != null) {
+                detail.setQuality(buildQualityMap(stats, days));
             }
 
             vo.setMaterialDetail(detail);
@@ -1294,75 +1325,71 @@ public class VectorRecallTestServiceImpl implements VectorRecallTestService {
         return result;
     }
 
-    private Map<String, Object> buildQualityMap(MaterialQuality mq) {
+    private Map<String, Object> buildQualityMap(AdsMaterialTouliuAllChannel s, int days) {
         Map<String, Object> map = new LinkedHashMap<>();
-        map.put("dt", mq.getDt());
-        map.put("qualityScore", mq.getQualityScore());
-        map.put("confidence", mq.getConfidence());
-        map.put("conversionEfficiencyScore", mq.getConversionEfficiencyScore());
-        map.put("revenueScore", mq.getRevenueScore());
-        map.put("viralScore", mq.getViralScore());
-        map.put("engagementScore", mq.getEngagementScore());
-        map.put("cost7d", mq.getCost7d());
-        map.put("targetConversion7d", mq.getTargetConversion7d());
-        map.put("totalConversion7d", mq.getTotalConversion7d());
-        map.put("revenue7d", mq.getRevenue7d());
-        map.put("t0ViralCount7d", mq.getT0ViralCount7d());
-        map.put("t0ViralRate7d", mq.getT0ViralRate7d());
-        map.put("miniProgramOpenRate7d", mq.getMiniProgramOpenRate7d());
-        map.put("firstUv7d", mq.getFirstUv7d());
-        map.put("shareCount7d", mq.getShareCount7d());
-        map.put("cost30d", mq.getCost30d());
-        map.put("targetConversion30d", mq.getTargetConversion30d());
-        // 派生指标
-        Long firstUv = mq.getFirstUv7d();
-        Long totalConv = mq.getTotalConversion7d();
-        Long viralCount = mq.getT0ViralCount7d();
-        Double rev = mq.getRevenue7d();
-
-        // 消耗优先7天,不足时用30天兜底
-        Double cost = mq.getCost7d();
-        boolean use30d = cost == null || cost < 50;
-        if (use30d && mq.getCost30d() != null && mq.getCost30d() > 0) {
-            cost = mq.getCost30d();
-        }
-        map.put("use30d", use30d);
-        map.put("effectiveCost", cost);  // 实际用于计算的消耗(7d 不足时兜底为 30d)
-
-        // 打开率(CTR) = 首层uv / 总转化量
-        if (firstUv != null && firstUv > 0 && totalConv != null && totalConv > 0) {
-            map.put("ctr7d", Math.round((double) firstUv / totalConv * 10000.0) / 10000.0);
-        }
-        // 裂变率 = t0裂变数 / 首层uv
-        if (viralCount != null && viralCount > 0 && firstUv != null && firstUv > 0) {
-            map.put("viralRate7d", Math.round((double) viralCount / firstUv * 10000.0) / 10000.0);
-        }
-        // ROI = 收入 / effectiveCost
-        if (cost != null && cost > 0 && rev != null && rev > 0) {
-            map.put("roi7d", Math.round(rev / cost * 10000.0) / 10000.0);
-        }
-        map.put("adOptimizationGoal", mq.getAdOptimizationGoal());
-        map.put("packageName", mq.getPackageName());
-        map.put("adStatus", mq.getAdStatus());
-        map.put("creativeStatus", mq.getCreativeStatus());
+        map.put("dt", String.valueOf(days));
+        map.put("impressions", s.getImpressions());
+        map.put("clicks", s.getClicks());
+        map.put("conversions", s.getConversions());
+        map.put("cost", s.getCost());
+        map.put("income", s.getIncome());
+        map.put("firstUv", s.getFirstUv());
+        map.put("fission0Uv", s.getFission0Uv());
+
+        // 效率指标
+        Double ctr = MaterialQualityCalculator.divide(s.getClicks(), s.getImpressions());
+        Double cvr = MaterialQualityCalculator.divide(s.getConversions(), s.getImpressions());
+        Double roi = MaterialQualityCalculator.divide(s.getIncome(), s.getCost());
+        Double openRate = MaterialQualityCalculator.divide(s.getFirstUv(), s.getConversions());
+        Double fissionRate = MaterialQualityCalculator.divide(s.getFission0Uv(), s.getFirstUv());
+        if (ctr != null) { map.put("ctr", MaterialQualityCalculator.round4(ctr)); map.put("ctrScore", MaterialQualityCalculator.round4(ctr)); }
+        if (cvr != null) { map.put("cvr", MaterialQualityCalculator.round4(cvr)); map.put("cvrScore", MaterialQualityCalculator.round4(cvr)); }
+        if (roi != null) { map.put("roi", MaterialQualityCalculator.round4(roi)); map.put("roiScore", MaterialQualityCalculator.round4(roi)); }
+        if (openRate != null) { map.put("openRate", MaterialQualityCalculator.round4(openRate)); map.put("openRateScore", MaterialQualityCalculator.round4(openRate)); }
+        if (fissionRate != null) { map.put("fissionRate", MaterialQualityCalculator.round4(fissionRate)); map.put("fissionRateScore", MaterialQualityCalculator.round4(fissionRate)); }
+
+        // 综合质量分(等权加权,测试路径无批内百分位排名,用原始比率近似)
+        double qs = computeSimpleQualityScore(s);
+        map.put("qualityScore", MaterialQualityCalculator.round4(qs));
+
         return map;
     }
 
-    private Map<String, MaterialQuality> loadMaterialQualityRows(List<String> materialIds) {
+    /** 简单质量分:加权求和,与 MaterialSearchServiceImpl 权重默认值保持一致 */
+    private double computeSimpleQualityScore(AdsMaterialTouliuAllChannel s) {
+        if (s == null) return 0;
+        double wCtr = 0.2, wCvr = 0.2, wRoi = 0.2, wOpenRate = 0.2, wFissionRate = 0.2;
+        Double ctr = MaterialQualityCalculator.divide(s.getClicks(), s.getImpressions());
+        Double cvr = MaterialQualityCalculator.divide(s.getConversions(), s.getImpressions());
+        Double roi = MaterialQualityCalculator.divide(s.getIncome(), s.getCost());
+        Double openRate = MaterialQualityCalculator.divide(s.getFirstUv(), s.getConversions());
+        Double fissionRate = MaterialQualityCalculator.divide(s.getFission0Uv(), s.getFirstUv());
+        return (ctr != null ? wCtr * ctr : 0)
+             + (cvr != null ? wCvr * cvr : 0)
+             + (roi != null ? wRoi * roi : 0)
+             + (openRate != null ? wOpenRate * openRate : 0)
+             + (fissionRate != null ? wFissionRate * fissionRate : 0);
+    }
+
+    private Map<String, AdsMaterialTouliuAllChannel> loadMaterialTouliuStats(
+            List<String> materialIds, int days) {
         if (CollectionUtils.isEmpty(materialIds)) {
             return Collections.emptyMap();
         }
-        Map<String, MaterialQuality> result = new HashMap<>();
+        Map<String, AdsMaterialTouliuAllChannel> result = new HashMap<>();
         try {
-            List<MaterialQuality> rows = materialQualityMapperExt.selectByMaterialIds(materialIds);
+            String dtThreshold = LocalDate.now().minusDays(days)
+                    .format(DateTimeFormatter.ofPattern("yyyyMMdd"));
+            List<AdsMaterialTouliuAllChannel> rows = adsMaterialTouliuAllChannelMapperExt
+                    .aggregateByMaterialIds(materialIds, dtThreshold);
             if (CollectionUtils.isEmpty(rows)) return result;
-            for (MaterialQuality row : rows) {
+            for (AdsMaterialTouliuAllChannel row : rows) {
                 if (row != null && row.getMaterialId() != null) {
                     result.putIfAbsent(row.getMaterialId(), row);
                 }
             }
         } catch (Exception e) {
-            log.error("批量加载 material_quality 失败: {}", e.getMessage(), e);
+            log.error("批量加载 ads_material_touliu_all_channel 失败: {}", e.getMessage(), e);
         }
         return result;
     }
@@ -1813,9 +1840,9 @@ public class VectorRecallTestServiceImpl implements VectorRecallTestService {
                     ? vo.getMaterialDetail().getQuality() : null;
             if (qualityMap != null && !qualityMap.isEmpty()) {
                 qs.setHasData(true);
-                qs.setCtr(toDoubleOrNull(qualityMap.get("conversionEfficiencyScore")));
-                qs.setViral(toDoubleOrNull(qualityMap.get("viralScore")));
-                qs.setRoi(toDoubleOrNull(qualityMap.get("revenueScore")));
+                qs.setCtr(toDoubleOrNull(qualityMap.get("ctr")));
+                qs.setViral(toDoubleOrNull(qualityMap.get("fissionRate")));
+                qs.setRoi(toDoubleOrNull(qualityMap.get("roi")));
             } else {
                 qs.setHasData(false);
             }
@@ -1973,7 +2000,7 @@ public class VectorRecallTestServiceImpl implements VectorRecallTestService {
                             .collect(Collectors.toList());
                     List<MaterialMatch> deduped = deduplicateMaterialMatches(matches, tn);
                     if (!deduped.isEmpty()) {
-                        allResults.addAll(enrichMaterialMatches(deduped, cc));
+                        allResults.addAll(enrichMaterialMatches(deduped, cc, metricsDays));
                     }
                 } catch (Exception e) {
                     log.error("matchByMaterialId 素材搜索失败 configCode={}: {}", cc, e.getMessage(), e);
@@ -2363,7 +2390,7 @@ public class VectorRecallTestServiceImpl implements VectorRecallTestService {
                     List<MaterialMatch> matches = materialVectorStoreService.searchTopNByRawVector(cc, rawEmbedding, ctn);
                     List<MaterialMatch> deduped = deduplicateMaterialMatches(matches, tn);
                     if (!deduped.isEmpty()) {
-                        allResults.addAll(enrichMaterialMatches(deduped, cc));
+                        allResults.addAll(enrichMaterialMatches(deduped, cc, metricsDays));
                     }
                 } catch (Exception e) {
                     log.error("matchByArticleId 素材搜索失败 configCode={}: {}", cc, e.getMessage(), e);

+ 13 - 0
core/src/main/java/com/tzld/videoVector/util/MaterialQualityCalculator.java

@@ -155,6 +155,19 @@ public class MaterialQualityCalculator {
 
     // ===== 辅助方法 =====
 
+    /** a / b,任一方为 null 或 b==0 时返回 null */
+    public static Double divide(Number a, Number b) {
+        if (a == null || b == null) return null;
+        double bd = b.doubleValue();
+        if (bd == 0) return null;
+        return a.doubleValue() / bd;
+    }
+
+    /** 四舍五入保留 4 位小数 */
+    public static double round4(double v) {
+        return Math.round(v * 10000.0) / 10000.0;
+    }
+
     private static double nullToZero(Double v) { return v == null ? 0 : v; }
     private static double nullToZero(Long v) { return v == null ? 0 : v; }
 

+ 35 - 0
core/src/main/resources/mapper/pgVector/ext/AdsMaterialTouliuAllChannelMapperExt.xml

@@ -0,0 +1,35 @@
+<?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">
+<mapper namespace="com.tzld.videoVector.dao.mapper.pgVector.ext.AdsMaterialTouliuAllChannelMapperExt">
+
+    <resultMap id="AggregateResultMap" type="com.tzld.videoVector.model.po.pgVector.AdsMaterialTouliuAllChannel">
+        <result column="material_id" property="materialId"/>
+        <result column="impressions" property="impressions"/>
+        <result column="clicks" property="clicks"/>
+        <result column="conversions" property="conversions"/>
+        <result column="cost" property="cost"/>
+        <result column="income" property="income"/>
+        <result column="first_uv" property="firstUv"/>
+        <result column="fission_0_uv" property="fission0Uv"/>
+    </resultMap>
+
+    <select id="aggregateByMaterialIds" resultMap="AggregateResultMap">
+        SELECT
+            material_id,
+            SUM(impressions) AS impressions,
+            SUM(clicks) AS clicks,
+            SUM(conversions) AS conversions,
+            SUM(cost_) AS cost,
+            SUM(income) AS income,
+            SUM(first_uv) AS first_uv,
+            SUM(fission_0_uv) AS fission_0_uv
+        FROM ads_material_touliu_all_channel
+        WHERE material_id IN
+        <foreach collection="materialIds" item="id" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+          AND dt &gt;= #{dtThreshold}
+        GROUP BY material_id
+    </select>
+
+</mapper>