V10.549 收斂比價新鮮度口徑
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
================================================================================
|
||||
|
||||
【已完成】
|
||||
- V10.549 收斂比價新鮮度 KPI 口徑:coverage cache 升到 v10,`expires_at IS NULL` 不再算進「可用比價 / decision ready」,改拆成 `unknown_freshness_matches` / `unknown_freshness_count`,避免沒有到期時間的舊資料被當成可直接決策的新鮮價格。Dashboard / daily / growth 同步顯示未知新鮮度與「未形成有效身份配對」,並把 PChome/MOMO 價格方向文案改成 `PChome 價格壓力` / `MOMO 價格優勢`,降低誤讀。
|
||||
- V10.548 接線更多 focused exact 舊候選回刷:把 matcher 已驗證可安全走 total-price 的 3W CLINIC 膠原蛋白粉底液 50ml x2、花美水 Moisture/Inclear 1.7g x3、KUSSEN 寶寶益菌屁屁膏 50ml 3 入、Lab52 齒妍堂嬰幼兒/汪汪隊牙刷 2 入接進 `_fetch_retryable_candidate_skus()` focused true-low / rescore 窄門。這只擴大「舊候選可被新版 matcher 重評」的入口,不改 `MIN_MATCH_SCORE`、hard veto、auto price write safety 或既有覆寫保護。
|
||||
- V10.547 強化單位價覆核洞察:`manual_unit_price_required` 不再只是人工狀態,覆核隊列與商品看板會重新帶出單位價換算、MOMO/PChome 單位價方向、差距百分比與處理建議;決策信封 / OpenClaw / PPT 摘要可讀到 `unit_price_insight`。人工覆核寫回也會保留原始 `match_diagnostic_json` / comparison mode / diagnostic codes,避免後續簡報、審計或 AI 策略只剩人工文案而失去 matcher 證據鏈。
|
||||
- V10.546 補近門檻舊候選回刷隊列:`run_retryable_candidate_revalidation()` 新增 `legacy_unmasked_attempt`,當最新狀態是 `no_result` / `refresh_no_result` / `expired_match` 時,可回撈同 SKU 早期近門檻候選交給最新版 matcher 重評;仍要求 candidate id、分數下限、無 hard veto、exact_identity,且不打開人工否決、單位價、identity_veto 或 protected existing match。
|
||||
|
||||
@@ -402,7 +402,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
|
||||
# ==========================================
|
||||
# 系統版本與路徑
|
||||
# ==========================================
|
||||
SYSTEM_VERSION = "V10.548"
|
||||
SYSTEM_VERSION = "V10.549"
|
||||
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
|
||||
public_url = PUBLIC_URL # 用於模板顯示
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
## 📅 詳細更新日誌 (考古存檔)
|
||||
|
||||
### 2026-06-01:PChome 比價新鮮度操作閉環
|
||||
- **V10.549 比價新鮮度 KPI 口徑收斂**: `fetch_competitor_coverage()` cache 升到 v10,`expires_at IS NULL` 不再混入 `fresh_matches` / `decision_ready_rate`,改拆成 `unknown_freshness_matches` 與 `unknown_freshness_rate`,讓「可用比價覆蓋率」只代表有明確未過期時間的 identity 價格。Dashboard、daily、growth 同步顯示未知新鮮度與「未形成有效身份配對」,第一屏資料時間改看最新有效 PChome 價格抓取,並把價格方向文案改為 `PChome 價格壓力` / `MOMO 價格優勢`。
|
||||
- **V10.548 focused exact 舊候選回刷接線**: `_fetch_retryable_candidate_skus()` 的 focused true-low / rescore 窄門新增 3W CLINIC 膠原蛋白粉底液 50ml x2、花美水 Moisture/Inclear 1.7g x3、KUSSEN 寶寶益菌屁屁膏 50ml 3 入、Lab52 齒妍堂嬰幼兒 / 汪汪隊牙刷 2 入。這些品線在 matcher 測試中已是 `exact / total_price / price_alert_exact`,本次只讓舊 `true_low_confidence` / `rescore_accepted_current` 候選能被新版 matcher 重新判斷;仍不放寬 `MIN_MATCH_SCORE`、hard veto、auto write safety 與 stronger existing match 保護。
|
||||
- **V10.547 單位價覆核洞察與證據鏈保留**: `manual_unit_price_required` 現在會和 `unit_comparable` 一樣重新產生單位價比較,並轉成 `unit_price_insight`,明確標示 PChome 或 MOMO 哪邊單位價較低、差距百分比、嚴重度與操作建議;Dashboard 覆核卡、商品列、決策信封與 OpenClaw/PPT 摘要都可讀到這個訊號。人工覆核寫回 `competitor_match_attempts` 時也會在欄位存在時保留原始 `match_diagnostic_json`、`comparison_mode`、`hard_veto`、`diagnostic_codes`,`competitor_match_reviews.candidate_diagnostic` 同步附帶 JSON 證據,避免人工閉環後只剩狀態文字。
|
||||
- **V10.546 近門檻舊候選回刷隊列補漏**: `run_retryable_candidate_revalidation()` 的候選來源不再只看每個 SKU 最新一筆 attempt。新增 `legacy_unmasked_attempt`,當最新狀態是 `no_result` / `refresh_no_result` / `expired_match` 時,可回撈同 SKU 早期 `low_score`、`recoverable_low_score`、`true_low_confidence` 或 `rescore_accepted_current` 的近門檻候選,再交給最新版 matcher 重評。此入口仍要求 candidate product id、分數下限、無 hard veto、`exact_identity`,且不打開人工否決、單位價、identity_veto 或 protected existing match,避免為了覆蓋率破壞安全邊界。
|
||||
|
||||
@@ -372,19 +372,19 @@ def _build_competitor_decision(momo_price, pchome_price, match_status=None):
|
||||
|
||||
if gap_pct >= 5:
|
||||
return {
|
||||
'label': 'PChome 優勢',
|
||||
'tone': 'win',
|
||||
'gap_amount': gap_amount,
|
||||
'gap_pct': gap_pct,
|
||||
'summary': 'PChome 較便宜,可加強曝光與轉換'
|
||||
}
|
||||
if gap_pct <= -5:
|
||||
return {
|
||||
'label': 'MOMO 威脅',
|
||||
'label': 'PChome 價格壓力',
|
||||
'tone': 'risk',
|
||||
'gap_amount': gap_amount,
|
||||
'gap_pct': gap_pct,
|
||||
'summary': 'MOMO 較便宜,需評估價格或促銷因應'
|
||||
'summary': 'PChome 較便宜,需評估 MOMO 價格、促銷或曝光策略'
|
||||
}
|
||||
if gap_pct <= -5:
|
||||
return {
|
||||
'label': 'MOMO 價格優勢',
|
||||
'tone': 'win',
|
||||
'gap_amount': gap_amount,
|
||||
'gap_pct': gap_pct,
|
||||
'summary': 'MOMO 較便宜,可優先檢查毛利與曝光機會'
|
||||
}
|
||||
return {
|
||||
'label': '價格接近',
|
||||
@@ -714,6 +714,7 @@ def _merge_competitor_review_context(overview, review_context):
|
||||
'decision_ready_count': int(coverage.get('decision_ready_matches') or coverage.get('fresh_matches') or 0),
|
||||
'decision_ready_rate': coverage.get('decision_ready_rate', 0),
|
||||
'stale_match_count': int(coverage.get('stale_matches') or 0),
|
||||
'unknown_freshness_count': int(coverage.get('unknown_freshness_matches') or 0),
|
||||
'pending_match_count': int(coverage.get('pending') or overview.get('pending_match_count') or 0),
|
||||
'review_queue_count': int(coverage.get('actionable_review_count') or len(review_queue) or 0),
|
||||
'manual_closed_count': int(coverage.get('manual_closed_count') or 0),
|
||||
@@ -863,6 +864,7 @@ def _load_competitor_decision_overview(session, latest_items=None):
|
||||
'decision_ready_count': 0,
|
||||
'decision_ready_rate': 0,
|
||||
'stale_match_count': 0,
|
||||
'unknown_freshness_count': 0,
|
||||
'pchome_advantage_count': 0,
|
||||
'momo_threat_count': 0,
|
||||
'near_count': 0,
|
||||
@@ -1721,6 +1723,7 @@ def _load_cached_competitor_overview_for_review(now_taipei, review_queue, review
|
||||
'match_rate': 0,
|
||||
'decision_ready_count': 0,
|
||||
'decision_ready_rate': 0,
|
||||
'unknown_freshness_count': 0,
|
||||
'pchome_advantage_count': 0,
|
||||
'momo_threat_count': 0,
|
||||
'near_count': 0,
|
||||
|
||||
@@ -778,7 +778,7 @@ def _cached_payload(cache_key: str, producer, ttl_seconds: int = COMPETITOR_INTE
|
||||
|
||||
def fetch_competitor_coverage(engine) -> dict:
|
||||
return _cached_payload(
|
||||
f"coverage:v9:floor={PCHOME_MATCH_SCORE_FLOOR}:manual_reviews=1:rescore=1:review_no_fresh=1:decision_ready=1:open_queue=1",
|
||||
f"coverage:v10:floor={PCHOME_MATCH_SCORE_FLOOR}:manual_reviews=1:rescore=1:review_no_fresh=1:decision_ready=1:open_queue=1:unknown_freshness=1",
|
||||
lambda: _fetch_competitor_coverage_uncached(engine),
|
||||
)
|
||||
|
||||
@@ -795,14 +795,18 @@ def _fetch_competitor_coverage_uncached(engine) -> dict:
|
||||
"valid_matches": 0,
|
||||
"fresh_matches": 0,
|
||||
"stale_matches": 0,
|
||||
"unknown_freshness_matches": 0,
|
||||
"pending": 0,
|
||||
"decision_ready_matches": 0,
|
||||
"identity_coverage_matches": 0,
|
||||
"identity_coverage_rate": 0,
|
||||
"pending_identity_count": 0,
|
||||
"stale_identity_count": 0,
|
||||
"unknown_freshness_count": 0,
|
||||
"not_decision_ready_count": 0,
|
||||
"match_rate": 0,
|
||||
"fresh_match_rate": 0,
|
||||
"unknown_freshness_rate": 0,
|
||||
"decision_ready_rate": 0,
|
||||
"last_decision_ready_crawled_at": None,
|
||||
"attempt_status": {},
|
||||
@@ -871,7 +875,17 @@ def _fetch_competitor_coverage_uncached(engine) -> dict:
|
||||
fresh_competitor AS (
|
||||
SELECT sku, crawled_at
|
||||
FROM identity_competitor
|
||||
WHERE expires_at IS NULL OR expires_at > CURRENT_TIMESTAMP
|
||||
WHERE expires_at > CURRENT_TIMESTAMP
|
||||
),
|
||||
unknown_freshness_competitor AS (
|
||||
SELECT sku, crawled_at
|
||||
FROM identity_competitor
|
||||
WHERE expires_at IS NULL
|
||||
),
|
||||
stale_competitor AS (
|
||||
SELECT sku, crawled_at
|
||||
FROM identity_competitor
|
||||
WHERE expires_at <= CURRENT_TIMESTAMP
|
||||
),
|
||||
{attempt_cte}
|
||||
SELECT
|
||||
@@ -884,9 +898,10 @@ def _fetch_competitor_coverage_uncached(engine) -> dict:
|
||||
JOIN fresh_competitor fc ON fc.sku = lm.sku) AS fresh_matches,
|
||||
(SELECT COUNT(*)
|
||||
FROM latest_momo lm
|
||||
JOIN identity_competitor ic ON ic.sku = lm.sku
|
||||
LEFT JOIN fresh_competitor fc ON fc.sku = lm.sku
|
||||
WHERE fc.sku IS NULL) AS stale_matches,
|
||||
JOIN stale_competitor sc ON sc.sku = lm.sku) AS stale_matches,
|
||||
(SELECT COUNT(*)
|
||||
FROM latest_momo lm
|
||||
JOIN unknown_freshness_competitor ufc ON ufc.sku = lm.sku) AS unknown_freshness_matches,
|
||||
(SELECT COUNT(*)
|
||||
FROM latest_momo lm
|
||||
LEFT JOIN identity_competitor ic ON ic.sku = lm.sku
|
||||
@@ -909,6 +924,7 @@ def _fetch_competitor_coverage_uncached(engine) -> dict:
|
||||
valid = int(rows[0].get("valid_matches") or 0) if rows else 0
|
||||
fresh = int(rows[0].get("fresh_matches") or 0) if rows else 0
|
||||
stale = int(rows[0].get("stale_matches") or 0) if rows else 0
|
||||
unknown_freshness = int(rows[0].get("unknown_freshness_matches") or 0) if rows else 0
|
||||
pending = int(rows[0].get("pending") or 0) if rows else 0
|
||||
statuses = {
|
||||
str(row.get("attempt_status")): int(row.get("status_count") or 0)
|
||||
@@ -924,14 +940,18 @@ def _fetch_competitor_coverage_uncached(engine) -> dict:
|
||||
"valid_matches": valid,
|
||||
"fresh_matches": fresh,
|
||||
"stale_matches": stale,
|
||||
"unknown_freshness_matches": unknown_freshness,
|
||||
"pending": pending,
|
||||
"decision_ready_matches": fresh,
|
||||
"identity_coverage_matches": valid,
|
||||
"identity_coverage_rate": round(valid / max(active, 1) * 100, 1),
|
||||
"pending_identity_count": pending,
|
||||
"stale_identity_count": stale,
|
||||
"unknown_freshness_count": unknown_freshness,
|
||||
"not_decision_ready_count": pending + stale + unknown_freshness,
|
||||
"match_rate": round(valid / max(active, 1) * 100, 1),
|
||||
"fresh_match_rate": round(fresh / max(valid, 1) * 100, 1),
|
||||
"unknown_freshness_rate": round(unknown_freshness / max(valid, 1) * 100, 1),
|
||||
"decision_ready_rate": round(fresh / max(active, 1) * 100, 1),
|
||||
"last_decision_ready_crawled_at": last_decision_ready_crawled_at,
|
||||
"attempt_status": statuses,
|
||||
|
||||
@@ -364,7 +364,11 @@
|
||||
<strong class="momo-mono">{{ comp_coverage.stale_matches | default(0) | number_format }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>待審/待補</span>
|
||||
<span>未知新鮮度</span>
|
||||
<strong class="momo-mono">{{ comp_coverage.unknown_freshness_matches | default(0) | number_format }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>未形成有效身份配對</span>
|
||||
<strong class="momo-mono">{{ comp_coverage.pending | default(0) | number_format }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<div class="dashboard-section-label">
|
||||
<span class="num momo-mono">01</span>
|
||||
<span class="title">比價監控總覽</span>
|
||||
<span class="meta momo-mono">LIVE · 更新於 {{ datetime_now }}</span>
|
||||
<span class="meta momo-mono">KPI · 最新有效價格 {{ overview.last_pchome_crawled or '待刷新' }}</span>
|
||||
</div>
|
||||
<div class="dashboard-kpi-grid">
|
||||
<div class="dashboard-kpi">
|
||||
@@ -23,16 +23,17 @@
|
||||
{{ overview.decision_ready_count | default(0) | number_format }} / {{ overview.total_active | default(total_products) | number_format }} ACTIVE
|
||||
· 身份 {{ overview.identity_coverage_rate | default(overview.match_rate | default(0)) }}%
|
||||
· 過期 {{ overview.stale_match_count | default(0) | number_format }}
|
||||
· 未設到期 {{ overview.unknown_freshness_count | default(0) | number_format }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-kpi is-accent">
|
||||
<div class="dashboard-kpi-label momo-mono">PChome 優勢</div>
|
||||
<div class="dashboard-kpi-label momo-mono">PChome 價格壓力</div>
|
||||
<div class="dashboard-kpi-value momo-mono">{{ overview.pchome_advantage_count | default(0) | number_format }}</div>
|
||||
<div class="dashboard-kpi-sub momo-mono">平均價差 +{{ overview.avg_advantage_gap | default(0) }}%</div>
|
||||
<div class="dashboard-kpi-sub momo-mono">PChome 較低 · 平均價差 +{{ overview.avg_advantage_gap | default(0) }}%</div>
|
||||
</div>
|
||||
<div class="dashboard-kpi">
|
||||
<div class="dashboard-kpi-label momo-mono">MOMO 威脅</div>
|
||||
<div class="dashboard-kpi-value momo-mono is-danger">{{ overview.momo_threat_count | default(0) | number_format }}</div>
|
||||
<div class="dashboard-kpi-label momo-mono">MOMO 價格優勢</div>
|
||||
<div class="dashboard-kpi-value momo-mono is-success">{{ overview.momo_threat_count | default(0) | number_format }}</div>
|
||||
<div class="dashboard-kpi-sub momo-mono">MOMO 價格低於 PChome</div>
|
||||
</div>
|
||||
<div class="dashboard-kpi">
|
||||
@@ -53,12 +54,12 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-kpi">
|
||||
<div class="dashboard-kpi-label momo-mono">可用資料新鮮度</div>
|
||||
<div class="dashboard-kpi-value momo-mono is-small">{{ '已更新' if overview.last_pchome_crawled else '待更新' }}</div>
|
||||
<div class="dashboard-kpi-label momo-mono">最新有效價格抓取</div>
|
||||
<div class="dashboard-kpi-value momo-mono is-small">{{ overview.last_pchome_crawled or '待刷新' }}</div>
|
||||
<div class="dashboard-kpi-sub momo-mono">
|
||||
{{ overview.last_pchome_crawled or '尚無 PChome 抓取紀錄' }}
|
||||
· 新鮮率 {{ overview.fresh_match_rate | default(0) }}%
|
||||
新鮮率 {{ overview.fresh_match_rate | default(0) }}%
|
||||
· 待刷新 {{ overview.stale_match_count | default(0) | number_format }}
|
||||
· 未設到期 {{ overview.unknown_freshness_count | default(0) | number_format }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -72,7 +73,7 @@
|
||||
<div class="dashboard-backfill-label momo-mono">PCHOME MATCH BACKFILL</div>
|
||||
<div class="dashboard-backfill-title">PChome 補抓產線</div>
|
||||
<div class="dashboard-backfill-meta momo-mono">
|
||||
待刷新 {{ overview.stale_match_count | default(0) | number_format }} · 待補抓 {{ overview.pending_match_count | default(0) | number_format }} · 待處理覆核 {{ overview.review_queue_count | default(0) | number_format }} · 人工閉環 {{ overview.manual_closed_count | default(0) | number_format }} · 單位價 {{ overview.unit_comparable_count | default(0) | number_format }}
|
||||
待刷新 {{ overview.stale_match_count | default(0) | number_format }} · 未設到期 {{ overview.unknown_freshness_count | default(0) | number_format }} · 待補抓 {{ overview.pending_match_count | default(0) | number_format }} · 待處理覆核 {{ overview.review_queue_count | default(0) | number_format }} · 人工閉環 {{ overview.manual_closed_count | default(0) | number_format }} · 單位價 {{ overview.unit_comparable_count | default(0) | number_format }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-backfill-progress" aria-hidden="true">
|
||||
|
||||
@@ -153,7 +153,9 @@
|
||||
<strong class="momo-mono">{{ coverage.fresh_match_rate | default(0) }}%</strong>
|
||||
<span>價格過期</span>
|
||||
<strong class="momo-mono">{{ coverage.stale_matches | default(0) | number_format }}</strong>
|
||||
<span>待審/待補</span>
|
||||
<span>未知新鮮度</span>
|
||||
<strong class="momo-mono">{{ coverage.unknown_freshness_matches | default(0) | number_format }}</strong>
|
||||
<span>未形成有效身份配對</span>
|
||||
<strong class="momo-mono">{{ coverage.pending | default(0) | number_format }}</strong>
|
||||
<span>需單位價覆核</span>
|
||||
<strong class="momo-mono">{{ coverage.unit_comparable_count | default(0) | number_format }}</strong>
|
||||
|
||||
@@ -75,16 +75,21 @@ def test_competitor_coverage_counts_only_active_product_intersection():
|
||||
"def _fetch_manual_review_summary", 1
|
||||
)[0]
|
||||
|
||||
assert "coverage:v9" in source
|
||||
assert "coverage:v10" in source
|
||||
assert "rescore_accepted_count" in coverage_source
|
||||
assert "(SELECT COUNT(*) FROM valid_competitor) AS valid_matches" not in coverage_source
|
||||
assert "identity_competitor AS" in coverage_source
|
||||
assert "fresh_competitor AS" in coverage_source
|
||||
assert "unknown_freshness_competitor AS" in coverage_source
|
||||
assert "WHERE expires_at > CURRENT_TIMESTAMP" in coverage_source
|
||||
assert "WHERE expires_at IS NULL" in coverage_source
|
||||
assert "FROM latest_momo lm\n JOIN identity_competitor ic ON ic.sku = lm.sku" in coverage_source
|
||||
assert "LEFT JOIN fresh_competitor fc ON fc.sku = lm.sku" in coverage_source
|
||||
assert "WHERE fc.sku IS NULL" in coverage_source
|
||||
assert "\"fresh_matches\": fresh" in coverage_source
|
||||
assert "\"stale_matches\": stale" in coverage_source
|
||||
assert "\"unknown_freshness_matches\": unknown_freshness" in coverage_source
|
||||
assert "\"not_decision_ready_count\": pending + stale + unknown_freshness" in coverage_source
|
||||
assert "\"decision_ready_matches\": fresh" in coverage_source
|
||||
assert "\"decision_ready_rate\": round(fresh / max(active, 1) * 100, 1)" in coverage_source
|
||||
assert "\"identity_coverage_matches\": valid" in coverage_source
|
||||
@@ -136,6 +141,8 @@ def test_competitor_review_queue_is_canonical_unit_price_handoff():
|
||||
assert "coverage.fresh_match_rate" in growth_template
|
||||
assert "coverage.decision_ready_rate" in growth_template
|
||||
assert "coverage.stale_matches" in growth_template
|
||||
assert "coverage.unknown_freshness_matches" in growth_template
|
||||
assert "未形成有效身份配對" in growth_template
|
||||
assert "coverage.unit_comparable_count" in growth_template
|
||||
assert "coverage.rescore_accepted_count" in growth_template
|
||||
assert "重算待人工覆核" in growth_template
|
||||
@@ -145,6 +152,7 @@ def test_competitor_review_queue_is_canonical_unit_price_handoff():
|
||||
assert "comp_coverage.rescore_accepted_count" in daily_template
|
||||
assert "重算待人工覆核" in daily_template
|
||||
assert "comp_coverage.stale_matches" in daily_template
|
||||
assert "comp_coverage.unknown_freshness_matches" in daily_template
|
||||
assert "comp_coverage.decision_ready_rate" in daily_template
|
||||
|
||||
|
||||
|
||||
@@ -421,8 +421,8 @@ def test_dashboard_v2_shows_pchome_competitor_pricing_and_links():
|
||||
assert "competitor_product_id" in route_source
|
||||
assert "https://24h.pchome.com.tw/prod/" in route_source
|
||||
assert "_build_competitor_decision(" in route_source
|
||||
assert "PChome 優勢" in route_source
|
||||
assert "MOMO 威脅" in route_source
|
||||
assert "PChome 價格壓力" in route_source
|
||||
assert "MOMO 價格優勢" in route_source
|
||||
assert "item['pchome_competitor']" in route_source
|
||||
assert "item['competitor_decision']" in route_source
|
||||
|
||||
|
||||
Reference in New Issue
Block a user