Procházet zdrojové kódy

Merge branch 'feature/luojunhui/20260610-material-stat' of Web/content_vector_recall into master

luojunhui před 2 dny
rodič
revize
30279c3eb5

+ 24 - 11
src/api/recall.ts

@@ -93,6 +93,12 @@ export async function recallMaterialWithQuality(param: {
   sourceType?: number
   alpha?: number
   simMin?: number
+  days?: number
+  wCtr?: number
+  wCvr?: number
+  wRoi?: number
+  wOpenRate?: number
+  wFissionRate?: number
 }): Promise<RecallResultVO> {
   const resp = await client.post<CommonResponse<RecallMaterialScoreVO>>(
     '/material/recallWithQuality',
@@ -103,6 +109,12 @@ export async function recallMaterialWithQuality(param: {
       sourceType: param.sourceType ?? 2,
       alpha: param.alpha,
       simMin: param.simMin,
+      days: param.days ?? 7,
+      wCtr: param.wCtr,
+      wCvr: param.wCvr,
+      wRoi: param.wRoi,
+      wOpenRate: param.wOpenRate,
+      wFissionRate: param.wFissionRate,
     },
   )
   const data = resp.data?.data
@@ -117,18 +129,19 @@ export async function recallMaterialWithQuality(param: {
     const hasQuality =
       it.qualityScore != null ||
       it.finalScore != null ||
-      (it.confidence != null && Number.isFinite(it.confidence))
+      it.ctr != null || it.cvr != null || it.roi != null
     const quality = hasQuality ? {
-      dt: it.dt, sim: it.sim, qualityScore: it.qualityScore, confidence: it.confidence,
-      finalScore: it.finalScore, conversionEfficiencyScore: it.conversionEfficiencyScore,
-      revenueScore: it.revenueScore, viralScore: it.viralScore, engagementScore: it.engagementScore,
-      cost7d: it.cost7d, targetConversion7d: it.targetConversion7d, totalConversion7d: it.totalConversion7d,
-      revenue7d: it.revenue7d, ctr7d: it.ctr7d, cvr7d: it.cvr7d, viralRate7d: it.viralRate7d,
-      roi7d: it.roi7d, cpa7d: it.cpa7d, roas7d: it.roas7d, t0ViralRate7d: it.t0ViralRate7d,
-      t0ViralCount7d: it.t0ViralCount7d, miniProgramOpenRate7d: it.miniProgramOpenRate7d,
-      firstUv7d: it.firstUv7d, shareCount7d: it.shareCount7d, cost30d: it.cost30d,
-      targetConversion30d: it.targetConversion30d, adOptimizationGoal: it.adOptimizationGoal,
-      packageName: it.packageName, adStatus: it.adStatus, creativeStatus: it.creativeStatus,
+      dt: it.dt, sim: it.sim, qualityScore: it.qualityScore,
+      finalScore: it.finalScore,
+      ctr: it.ctr, cvr: it.cvr, roi: it.roi,
+      openRate: it.openRate, fissionRate: it.fissionRate,
+      impressions: it.impressions, clicks: it.clicks,
+      conversions: it.conversions, cost: it.cost,
+      income: it.income, firstUv: it.firstUv,
+      fission0Uv: it.fission0Uv,
+      ctrScore: it.ctrScore, cvrScore: it.cvrScore,
+      roiScore: it.roiScore, openRateScore: it.openRateScore,
+      fissionRateScore: it.fissionRateScore,
     } : null
 
     return {

+ 26 - 30
src/api/types.ts

@@ -146,9 +146,11 @@ export interface RecallSignals {
   /** 素材质量,缺失时 hasData=false */
   quality: {
     hasData: boolean
-    ctr: number | null   // conversionEfficiencyScore(素材)
-    viral: number | null // viralScore(素材)
-    roi: number | null   // revenueScore(素材)
+    ctr: number | null   // ctrScore(素材 CTR 百分位)
+    cvr: number | null   // cvrScore(素材 CVR 百分位)
+    viral: number | null // fissionRateScore(素材 T0裂变率 百分位)
+    roi: number | null   // roiScore(素材 ROI 百分位)
+    openRateScore: number | null // openRateScore(素材小程序打开率 百分位)
     // 文章质量维度分(ARTICLE 模态专用)
     readScore: number | null
     openScore: number | null
@@ -167,38 +169,32 @@ export interface RecallSignals {
   }
 }
 
-/** 素材质量/投放统计信息 (来源于 material_quality 表) */
+/** 素材质量/投放统计信息 (来源于 ads_material_touliu_all_channel 聚合) */
 export interface MaterialQualityInfo {
   dt?: string
   sim?: number
   qualityScore?: number
-  confidence?: number
   finalScore?: number
-  conversionEfficiencyScore?: number
-  revenueScore?: number
-  viralScore?: number
-  engagementScore?: number
-  cost7d?: number
-  targetConversion7d?: number
-  totalConversion7d?: number
-  revenue7d?: number
-  ctr7d?: number
-  cvr7d?: number
-  viralRate7d?: number
-  roi7d?: number
-  roas7d?: number
-  cpa7d?: number
-  t0ViralRate7d?: number
-  t0ViralCount7d?: number
-  miniProgramOpenRate7d?: number
-  firstUv7d?: number
-  shareCount7d?: number
-  cost30d?: number
-  targetConversion30d?: number
-  adOptimizationGoal?: string
-  packageName?: string
-  adStatus?: string
-  creativeStatus?: string
+  // 效率指标
+  ctr?: number
+  cvr?: number
+  roi?: number
+  openRate?: number
+  fissionRate?: number
+  // 原始投放数据求和
+  impressions?: number
+  clicks?: number
+  conversions?: number
+  cost?: number
+  income?: number
+  firstUv?: number
+  fission0Uv?: number
+  // 各维度百分位得分
+  ctrScore?: number
+  cvrScore?: number
+  roiScore?: number
+  openRateScore?: number
+  fissionRateScore?: number
 }
 
 /** 素材详情 - modality=MATERIAL 专用 */

+ 29 - 8
src/components/RankingSettingsButton.tsx

@@ -34,7 +34,8 @@ const FORMULA_VIDEO = `综合得分 = α·c·sim_norm + (1-α)·rov_norm  (c 仅
 
 const FORMULA_MATERIAL = `综合得分 = α·c·sim_norm + (1-α)·qualityScore  (c 仅作用于相关性分)
   sim_norm = clip((sim - simThreshold) / (1 - simThreshold), 0, 1)
-  qualityScore = (wCtr×ctr + wViral×viral + wRoi×roi) / (wCtr+wViral+wRoi)
+  qualityScore = wCtr×ctr + wCvr×cvr + wRoi×roi + wOpenRate×openRate + wFissionRate×fissionRate
+  各维度直接用原始效率比率
   c = boostsByCode[configCode] (默认 1) / deconstructBoost (未知维度兜底)
 先按 simThreshold 硬筛, 再按综合分排序.`
 
@@ -233,25 +234,25 @@ function MaterialWeightRows({ params, update }: { params: RankingParams; update:
         />
         <span style={{ width: 48, textAlign: 'right' }}>{params.alpha.toFixed(2)}</span>
       </Row>
-      <Row label="打开率 wCtr" tip="CTR 百分位的权重">
+      <Row label="CTR wCtr" tip="CTR 百分位的权重">
         <Slider
           min={0}
           max={1}
           step={0.05}
           value={params.wCtr}
-          onChange={(v) => update({ wCtr: typeof v === 'number' ? v : 0.3 })}
+          onChange={(v) => update({ wCtr: typeof v === 'number' ? v : 0.2 })}
         />
         <span style={{ width: 48, textAlign: 'right' }}>{params.wCtr.toFixed(2)}</span>
       </Row>
-      <Row label="裂变率 wViral" tip="裂变率百分位的权重">
+      <Row label="CVR wCvr" tip="CVR 百分位的权重">
         <Slider
           min={0}
           max={1}
           step={0.05}
-          value={params.wViral}
-          onChange={(v) => update({ wViral: typeof v === 'number' ? v : 0.2 })}
+          value={params.wCvr}
+          onChange={(v) => update({ wCvr: typeof v === 'number' ? v : 0.2 })}
         />
-        <span style={{ width: 48, textAlign: 'right' }}>{params.wViral.toFixed(2)}</span>
+        <span style={{ width: 48, textAlign: 'right' }}>{params.wCvr.toFixed(2)}</span>
       </Row>
       <Row label="ROI wRoi" tip="ROI 百分位的权重">
         <Slider
@@ -259,10 +260,30 @@ function MaterialWeightRows({ params, update }: { params: RankingParams; update:
           max={1}
           step={0.05}
           value={params.wRoi}
-          onChange={(v) => update({ wRoi: typeof v === 'number' ? v : 0.1 })}
+          onChange={(v) => update({ wRoi: typeof v === 'number' ? v : 0.2 })}
         />
         <span style={{ width: 48, textAlign: 'right' }}>{params.wRoi.toFixed(2)}</span>
       </Row>
+      <Row label="打开率 wOpenRate" tip="小程序打开率百分位的权重">
+        <Slider
+          min={0}
+          max={1}
+          step={0.05}
+          value={params.wOpenRate}
+          onChange={(v) => update({ wOpenRate: typeof v === 'number' ? v : 0.2 })}
+        />
+        <span style={{ width: 48, textAlign: 'right' }}>{params.wOpenRate.toFixed(2)}</span>
+      </Row>
+      <Row label="裂变率 wFissionRate" tip="T0裂变率百分位的权重">
+        <Slider
+          min={0}
+          max={1}
+          step={0.05}
+          value={params.wFissionRate}
+          onChange={(v) => update({ wFissionRate: typeof v === 'number' ? v : 0.2 })}
+        />
+        <span style={{ width: 48, textAlign: 'right' }}>{params.wFissionRate.toFixed(2)}</span>
+      </Row>
     </>
   )
 }

+ 23 - 5
src/components/RankingWeightsPanel.tsx

@@ -66,7 +66,7 @@ export default function RankingWeightsPanel({
       {showMaterialQuality && (
         <RankingRow label="素材质量">
           <InlineSlider
-            label="打开率 wCtr"
+            label="CTR wCtr"
             value={params.wCtr}
             min={0}
             max={1}
@@ -75,12 +75,12 @@ export default function RankingWeightsPanel({
             width={120}
           />
           <InlineSlider
-            label="裂变率 wViral"
-            value={params.wViral}
+            label="CVR wCvr"
+            value={params.wCvr}
             min={0}
             max={1}
             step={0.05}
-            onChange={(v) => update({ wViral: v })}
+            onChange={(v) => update({ wCvr: v })}
             width={120}
           />
           <InlineSlider
@@ -92,6 +92,24 @@ export default function RankingWeightsPanel({
             onChange={(v) => update({ wRoi: v })}
             width={120}
           />
+          <InlineSlider
+            label="打开率 wOpenRate"
+            value={params.wOpenRate}
+            min={0}
+            max={1}
+            step={0.05}
+            onChange={(v) => update({ wOpenRate: v })}
+            width={120}
+          />
+          <InlineSlider
+            label="裂变率 wFissionRate"
+            value={params.wFissionRate}
+            min={0}
+            max={1}
+            step={0.05}
+            onChange={(v) => update({ wFissionRate: v })}
+            width={120}
+          />
         </RankingRow>
       )}
 
@@ -167,7 +185,7 @@ export default function RankingWeightsPanel({
         <Text type="secondary" style={{ fontSize: 11 }}>
           α={params.alpha} | sim≥{params.simThreshold}
           {showMaterialQuality
-            ? ` | 素材: ctr=${params.wCtr} viral=${params.wViral} roi=${params.wRoi}`
+            ? ` | 素材: ctr=${params.wCtr} cvr=${params.wCvr} roi=${params.wRoi} open=${params.wOpenRate} fission=${params.wFissionRate}`
             : ''}
           {showArticleQuality
             ? ` | 文章: read=${params.wRead} open=${params.wOpen} fission=${params.wFission}`

+ 101 - 47
src/components/RecallResultTable.tsx

@@ -759,7 +759,7 @@ export default function RecallResultTable({
     },
   }
 
-  /** 素材质量列 (MATERIAL Tab) */
+  /** 素材质量列 (MATERIAL Tab) — 效率指标 + 百分位得分 */
   const materialQualityCols: ColumnsType<RowItem> = [
     {
       title: '质量分',
@@ -772,105 +772,157 @@ export default function RecallResultTable({
       render: (_v, item) => <WeightedQualityCell item={item} rankingParams={rankingParams} />,
     },
     {
-      title: '近7日打开率',
-      key: 'q.ctr7d',
-      width: 100,
+      title: 'CTR',
+      key: 'q.ctr',
+      width: 80,
       align: 'right',
-      sorter: (a, b) => (a.materialDetail?.quality?.ctr7d ?? -1) - (b.materialDetail?.quality?.ctr7d ?? -1),
+      sorter: (a, b) => (a.materialDetail?.quality?.ctr ?? -1) - (b.materialDetail?.quality?.ctr ?? -1),
       sortDirections: ['descend', 'ascend'],
       render: (_v, item) => {
-        const v = item.materialDetail?.quality?.ctr7d
+        const v = item.materialDetail?.quality?.ctr
         if (v == null) return <Text type="secondary">--</Text>
         return <span style={{ fontVariantNumeric: 'tabular-nums', fontSize: 12 }}>{(v * 100).toFixed(2)}%</span>
       },
     },
     {
-      title: '近7日裂变率',
-      key: 'q.viralRate7d',
-      width: 100,
+      title: 'CVR',
+      key: 'q.cvr',
+      width: 80,
       align: 'right',
-      sorter: (a, b) => (a.materialDetail?.quality?.viralRate7d ?? -1) - (b.materialDetail?.quality?.viralRate7d ?? -1),
+      sorter: (a, b) => (a.materialDetail?.quality?.cvr ?? -1) - (b.materialDetail?.quality?.cvr ?? -1),
       sortDirections: ['descend', 'ascend'],
       render: (_v, item) => {
-        const v = item.materialDetail?.quality?.viralRate7d
+        const v = item.materialDetail?.quality?.cvr
         if (v == null) return <Text type="secondary">--</Text>
         return <span style={{ fontVariantNumeric: 'tabular-nums', fontSize: 12 }}>{(v * 100).toFixed(2)}%</span>
       },
     },
     {
-      title: '近7日ROI',
-      key: 'q.roi7d',
-      width: 90,
+      title: 'ROI',
+      key: 'q.roi',
+      width: 80,
       align: 'right',
-      sorter: (a, b) => (a.materialDetail?.quality?.roi7d ?? -1) - (b.materialDetail?.quality?.roi7d ?? -1),
+      sorter: (a, b) => (a.materialDetail?.quality?.roi ?? -1) - (b.materialDetail?.quality?.roi ?? -1),
       sortDirections: ['descend', 'ascend'],
       render: (_v, item) => {
-        const v = item.materialDetail?.quality?.roi7d
+        const v = item.materialDetail?.quality?.roi
         if (v == null) return <Text type="secondary">--</Text>
         return <span style={{ fontVariantNumeric: 'tabular-nums', fontSize: 12 }}>{v.toFixed(2)}</span>
       },
     },
     {
-      title: '近7日转化',
-      key: 'q.totalConversion7d',
-      width: 110,
+      title: '打开率',
+      key: 'q.openRate',
+      width: 80,
+      align: 'right',
+      sorter: (a, b) => (a.materialDetail?.quality?.openRate ?? -1) - (b.materialDetail?.quality?.openRate ?? -1),
+      sortDirections: ['descend', 'ascend'],
+      render: (_v, item) => {
+        const v = item.materialDetail?.quality?.openRate
+        if (v == null) return <Text type="secondary">--</Text>
+        return <span style={{ fontVariantNumeric: 'tabular-nums', fontSize: 12 }}>{(v * 100).toFixed(2)}%</span>
+      },
+    },
+    {
+      title: '裂变率',
+      key: 'q.fissionRate',
+      width: 80,
+      align: 'right',
+      sorter: (a, b) => (a.materialDetail?.quality?.fissionRate ?? -1) - (b.materialDetail?.quality?.fissionRate ?? -1),
+      sortDirections: ['descend', 'ascend'],
+      render: (_v, item) => {
+        const v = item.materialDetail?.quality?.fissionRate
+        if (v == null) return <Text type="secondary">--</Text>
+        return <span style={{ fontVariantNumeric: 'tabular-nums', fontSize: 12 }}>{(v * 100).toFixed(2)}%</span>
+      },
+    },
+    {
+      title: '曝光',
+      key: 'q.impressions',
+      width: 90,
       align: 'right',
-      sorter: (a, b) => (a.materialDetail?.quality?.totalConversion7d ?? -1) - (b.materialDetail?.quality?.totalConversion7d ?? -1),
+      sorter: (a, b) => (a.materialDetail?.quality?.impressions ?? -1) - (b.materialDetail?.quality?.impressions ?? -1),
       sortDirections: ['descend', 'ascend'],
       render: (_v, item) => {
-        const v = item.materialDetail?.quality?.totalConversion7d
+        const v = item.materialDetail?.quality?.impressions
         if (v == null) return <Text type="secondary">--</Text>
         return <span style={{ fontVariantNumeric: 'tabular-nums', fontSize: 12 }}>{formatNumber(String(v))}</span>
       },
     },
     {
-      title: '近7日首层UV',
-      key: 'q.firstUv7d',
-      width: 110,
+      title: '点击',
+      key: 'q.clicks',
+      width: 90,
       align: 'right',
-      sorter: (a, b) => (a.materialDetail?.quality?.firstUv7d ?? -1) - (b.materialDetail?.quality?.firstUv7d ?? -1),
+      sorter: (a, b) => (a.materialDetail?.quality?.clicks ?? -1) - (b.materialDetail?.quality?.clicks ?? -1),
       sortDirections: ['descend', 'ascend'],
       render: (_v, item) => {
-        const v = item.materialDetail?.quality?.firstUv7d
+        const v = item.materialDetail?.quality?.clicks
         if (v == null) return <Text type="secondary">--</Text>
         return <span style={{ fontVariantNumeric: 'tabular-nums', fontSize: 12 }}>{formatNumber(String(v))}</span>
       },
     },
     {
-      title: '近7日裂变',
-      key: 'q.t0ViralCount7d',
-      width: 100,
+      title: '转化',
+      key: 'q.conversions',
+      width: 90,
       align: 'right',
-      sorter: (a, b) => (a.materialDetail?.quality?.t0ViralCount7d ?? -1) - (b.materialDetail?.quality?.t0ViralCount7d ?? -1),
+      sorter: (a, b) => (a.materialDetail?.quality?.conversions ?? -1) - (b.materialDetail?.quality?.conversions ?? -1),
       sortDirections: ['descend', 'ascend'],
       render: (_v, item) => {
-        const v = item.materialDetail?.quality?.t0ViralCount7d
+        const v = item.materialDetail?.quality?.conversions
         if (v == null) return <Text type="secondary">--</Text>
         return <span style={{ fontVariantNumeric: 'tabular-nums', fontSize: 12 }}>{formatNumber(String(v))}</span>
       },
     },
     {
-      title: '近7日收入',
-      key: 'q.revenue7d',
-      width: 100,
+      title: '成本',
+      key: 'q.cost',
+      width: 90,
+      align: 'right',
+      sorter: (a, b) => (a.materialDetail?.quality?.cost ?? -1) - (b.materialDetail?.quality?.cost ?? -1),
+      sortDirections: ['descend', 'ascend'],
+      render: (_v, item) => {
+        const v = item.materialDetail?.quality?.cost
+        if (v == null) return <Text type="secondary">--</Text>
+        return <span style={{ fontVariantNumeric: 'tabular-nums', fontSize: 12 }}>{v.toFixed(2)}</span>
+      },
+    },
+    {
+      title: '收入',
+      key: 'q.income',
+      width: 90,
+      align: 'right',
+      sorter: (a, b) => (a.materialDetail?.quality?.income ?? -1) - (b.materialDetail?.quality?.income ?? -1),
+      sortDirections: ['descend', 'ascend'],
+      render: (_v, item) => {
+        const v = item.materialDetail?.quality?.income
+        if (v == null) return <Text type="secondary">--</Text>
+        return <span style={{ fontVariantNumeric: 'tabular-nums', fontSize: 12 }}>{v.toFixed(2)}</span>
+      },
+    },
+    {
+      title: '首层UV',
+      key: 'q.firstUv',
+      width: 90,
       align: 'right',
-      sorter: (a, b) => (a.materialDetail?.quality?.revenue7d ?? -1) - (b.materialDetail?.quality?.revenue7d ?? -1),
+      sorter: (a, b) => (a.materialDetail?.quality?.firstUv ?? -1) - (b.materialDetail?.quality?.firstUv ?? -1),
       sortDirections: ['descend', 'ascend'],
       render: (_v, item) => {
-        const v = item.materialDetail?.quality?.revenue7d
+        const v = item.materialDetail?.quality?.firstUv
         if (v == null) return <Text type="secondary">--</Text>
         return <span style={{ fontVariantNumeric: 'tabular-nums', fontSize: 12 }}>{formatNumber(String(v))}</span>
       },
     },
     {
-      title: '近7日消耗',
-      key: 'q.cost7d',
-      width: 100,
+      title: 'T0裂变',
+      key: 'q.fission0Uv',
+      width: 90,
       align: 'right',
-      sorter: (a, b) => (a.materialDetail?.quality?.cost7d ?? -1) - (b.materialDetail?.quality?.cost7d ?? -1),
+      sorter: (a, b) => (a.materialDetail?.quality?.fission0Uv ?? -1) - (b.materialDetail?.quality?.fission0Uv ?? -1),
       sortDirections: ['descend', 'ascend'],
       render: (_v, item) => {
-        const v = item.materialDetail?.quality?.cost7d
+        const v = item.materialDetail?.quality?.fission0Uv
         if (v == null) return <Text type="secondary">--</Text>
         return <span style={{ fontVariantNumeric: 'tabular-nums', fontSize: 12 }}>{formatNumber(String(v))}</span>
       },
@@ -878,14 +930,14 @@ export default function RecallResultTable({
   ]
 
   const statsDateColumn: ColumnsType<RowItem>[number] = {
-    title: '统计日期',
+    title: '统计天数',
     key: 'q.dt',
-    width: 90,
+    width: 80,
     align: 'center',
     render: (_v, item) => {
       const v = item.materialDetail?.quality?.dt
       if (v == null) return <Text type="secondary">--</Text>
-      return <span style={{ fontSize: 12 }}>{v}</span>
+      return <span style={{ fontSize: 12 }}>{v}</span>
     },
   }
 
@@ -1296,6 +1348,9 @@ function CompositeScoreCell({
       <div style={{ fontSize: 12, lineHeight: 1.6 }}>
         <div>相关性分 sim_norm = {simNorm.toFixed(3)}</div>
         <div>素材质量 quality = {weightedQuality != null ? weightedQuality.toFixed(3) : '--'}</div>
+        <div style={{ color: 'rgba(255,255,255,0.5)', fontSize: 10 }}>
+          = wCtr·ctr + wCvr·cvr + wRoi·roi + wOpenRate·openRate + wFissionRate·fissionRate
+        </div>
         <div>c = {boost.toFixed(2)}</div>
         <div style={{ borderTop: '1px solid rgba(255,255,255,0.3)', marginTop: 4, paddingTop: 4 }}>
           composite = α·c·sim_norm + (1-α)·quality = <b>{text}</b>
@@ -1369,11 +1424,10 @@ function WeightedQualityCell({
   const tip = (
     <div style={{ fontSize: 12, lineHeight: 1.6 }}>
       <div>
-        quality = (wCtr·ctr + wViral·viral + wRoi·roi) / Σw
+        quality = wCtr·ctr + wCvr·cvr + wRoi·roi + wOpenRate·openRate + wFissionRate·fissionRate
       </div>
       <div>
-        wCtr={rankingParams.wCtr.toFixed(2)} wViral={rankingParams.wViral.toFixed(2)} wRoi=
-        {rankingParams.wRoi.toFixed(2)}
+        wCtr={rankingParams.wCtr.toFixed(2)} wCvr={rankingParams.wCvr.toFixed(2)} wRoi={rankingParams.wRoi.toFixed(2)} wOpenRate={rankingParams.wOpenRate.toFixed(2)} wFissionRate={rankingParams.wFissionRate.toFixed(2)}
       </div>
       {offline != null && (
         <div style={{ marginTop: 4, color: 'rgba(255,255,255,0.65)' }}>

+ 32 - 18
src/utils/scoring.ts

@@ -17,10 +17,9 @@ import type { RecallSignals, VideoMatchEnrichedVO } from '../api/types'
  *
  * 公式 (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 × boost × sim_norm + (1 - alpha) × qualityScore
- *   质量缺失时按 materialMissingStrategy 处理:"group"(分组)或 "shrink"(收缩)
- *   boost 仅作用于相关性分
+ *   各维度直接用原始效率比率,无投放数据素材 qualityScore = 0
  */
 export interface RankingParams {
   simThreshold: number
@@ -35,12 +34,16 @@ export interface RankingParams {
   deconstructBoost: number
   /** 按维度独立 boost —— 每个 configCode 可单独设置,覆盖维度默认值 */
   boostsByCode: Record<string, number>
-  /** 素材质量子维度权重——打开率,默认 0.5(与 wViral/wRoi 之和为 1) */
+  /** 素材质量子维度权重——CTR 百分位,默认 0.2 */
   wCtr: number
-  /** 素材质量子维度权重——裂变率,默认 0.3 */
-  wViral: number
-  /** 素材质量子维度权重——ROI,默认 0.2 */
+  /** 素材质量子维度权重——CVR 百分位,默认 0.2 */
+  wCvr: number
+  /** 素材质量子维度权重——ROI 百分位,默认 0.2 */
   wRoi: number
+  /** 素材质量子维度权重——小程序打开率 百分位,默认 0.2 */
+  wOpenRate: number
+  /** 素材质量子维度权重——T0裂变率 百分位,默认 0.2 */
+  wFissionRate: number
   /** 素材质量缺失策略:"group" | "shrink" */
   materialMissingStrategy: 'group' | 'shrink'
   /** 文章质量子维度权重——阅读,默认 0.4 */
@@ -70,9 +73,11 @@ export const DEFAULT_RANKING_PARAMS: RankingParams = {
   rovClipHigh: 0.07,
   alpha: 0.6,
   deconstructBoost: 0.4,
-  wCtr: 0.5,
-  wViral: 0.3,
+  wCtr: 0.2,
+  wCvr: 0.2,
   wRoi: 0.2,
+  wOpenRate: 0.2,
+  wFissionRate: 0.2,
   materialMissingStrategy: 'group',
   wRead: 0.4,
   wOpen: 0.3,
@@ -86,7 +91,7 @@ export interface ScoreBreakdown {
   boost: number
   lowerBound: number
   passesThreshold: boolean
-  /** 精排加权质量分:素材=(wCtr·ctr+wViral·viral+wRoi·roi)/Σw;视频=rov_norm */
+  /** 精排加权质量分:素材=wCtr·ctrScore+wCvr·cvrScore+wRoi·roiScore+wOpenRate·openRateScore+wFissionRate·fissionRateScore;视频=rov_norm */
   weightedQuality?: number
   /** 素材质量缺失时置 true,调用方按策略单独成组 */
   qualityMissing?: boolean
@@ -100,16 +105,20 @@ function materialQualityFromDetail(
 ): RecallSignals['quality'] | undefined {
   const q = item.materialDetail?.quality
   if (!q) return undefined
-  const ctr = q.conversionEfficiencyScore
-  const viral = q.viralScore
-  const roi = q.revenueScore
-  const hasData = [ctr, viral, roi].some((v) => v != null && Number.isFinite(v))
+  const ctr = q.ctrScore
+  const cvr = q.cvrScore
+  const viral = q.fissionRateScore
+  const roi = q.roiScore
+  const openRateScore = q.openRateScore
+  const hasData = [ctr, cvr, viral, roi, openRateScore].some((v) => v != null && Number.isFinite(v))
   if (!hasData) return undefined
   return {
     hasData: true,
     ctr: ctr ?? null,
+    cvr: cvr ?? null,
     viral: viral ?? null,
     roi: roi ?? null,
+    openRateScore: openRateScore ?? null,
     readScore: null,
     openScore: null,
     fissionScore: null,
@@ -203,10 +212,12 @@ function rankMaterial(
   }
 
   const ctr = quality.ctr ?? 0
+  const cvr = quality.cvr ?? 0
   const viral = quality.viral ?? 0
   const roi = quality.roi ?? 0
-  const qualTotalW = params.wCtr + params.wViral + params.wRoi || 1
-  const weightedQuality = (params.wCtr * ctr + params.wViral * viral + params.wRoi * roi) / qualTotalW
+  const openRate = quality.openRateScore ?? 0
+  const weightedQuality = params.wCtr * ctr + params.wCvr * cvr + params.wRoi * roi
+    + params.wOpenRate * openRate + params.wFissionRate * viral
   const composite = alpha * codeBoost * simNorm + (1 - alpha) * weightedQuality
   return {
     composite,
@@ -263,7 +274,7 @@ function rankVideoArticle(
 
 const STORAGE_KEY = 'vector_recall_ranking_params'
 /** 维度 boost 默认值变更版本;升级后清理旧版 localStorage 脏数据 */
-const RANKING_STORAGE_VERSION = 2
+const RANKING_STORAGE_VERSION = 3
 const RANKING_VERSION_KEY = 'vector_recall_ranking_params_version'
 
 /** 旧版 UI 常见落盘值(deconstructBoost 兜底 / 批量微调),不等于新版维度默认时应清除 */
@@ -313,6 +324,7 @@ function loadFromStorage(): RankingParams {
     delete parsed.rovP5
     delete parsed.rovP95
     delete parsed.wSim
+    delete parsed.wViral  // 旧版 3-weights → 新版 5-weights
 
     const boostsByCode = needsBoostMigration
       ? sanitizeBoostsByCode(parsed.boostsByCode as Record<string, number> | undefined)
@@ -377,8 +389,10 @@ export function toRankingPayload(params: RankingParams): RankingParams {
     deconstructBoost: params.deconstructBoost,
     boostsByCode: params.boostsByCode ?? {},
     wCtr: params.wCtr,
-    wViral: params.wViral,
+    wCvr: params.wCvr,
     wRoi: params.wRoi,
+    wOpenRate: params.wOpenRate,
+    wFissionRate: params.wFissionRate,
     materialMissingStrategy: params.materialMissingStrategy,
     wRead: params.wRead,
     wOpen: params.wOpen,