diff --git a/TODO_NEXT_STEPS.txt b/TODO_NEXT_STEPS.txt index 75c7c33..a08beb5 100644 --- a/TODO_NEXT_STEPS.txt +++ b/TODO_NEXT_STEPS.txt @@ -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。 diff --git a/config.py b/config.py index 0b0b2a2..f3c2083 100644 --- a/config.py +++ b/config.py @@ -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 # 用於模板顯示 diff --git a/docs/memory/history_logs.md b/docs/memory/history_logs.md index 08957f9..2fbb022 100644 --- a/docs/memory/history_logs.md +++ b/docs/memory/history_logs.md @@ -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,避免為了覆蓋率破壞安全邊界。 diff --git a/routes/dashboard_routes.py b/routes/dashboard_routes.py index 6b18168..57e7968 100644 --- a/routes/dashboard_routes.py +++ b/routes/dashboard_routes.py @@ -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, diff --git a/services/competitor_intel_repository.py b/services/competitor_intel_repository.py index facdc9a..a2394ba 100644 --- a/services/competitor_intel_repository.py +++ b/services/competitor_intel_repository.py @@ -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, diff --git a/templates/daily_sales.html b/templates/daily_sales.html index 8c5525b..4bd8ff7 100644 --- a/templates/daily_sales.html +++ b/templates/daily_sales.html @@ -364,7 +364,11 @@ {{ comp_coverage.stale_matches | default(0) | number_format }}
- 待審/待補 + 未知新鮮度 + {{ comp_coverage.unknown_freshness_matches | default(0) | number_format }} +
+
+ 未形成有效身份配對 {{ comp_coverage.pending | default(0) | number_format }}
diff --git a/templates/dashboard_v2.html b/templates/dashboard_v2.html index 51d98c2..0079fc3 100644 --- a/templates/dashboard_v2.html +++ b/templates/dashboard_v2.html @@ -13,7 +13,7 @@
01 比價監控總覽 - LIVE · 更新於 {{ datetime_now }} + KPI · 最新有效價格 {{ overview.last_pchome_crawled or '待刷新' }}
@@ -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 }}
-
PChome 優勢
+
PChome 價格壓力
{{ overview.pchome_advantage_count | default(0) | number_format }}
-
平均價差 +{{ overview.avg_advantage_gap | default(0) }}%
+
PChome 較低 · 平均價差 +{{ overview.avg_advantage_gap | default(0) }}%
-
MOMO 威脅
-
{{ overview.momo_threat_count | default(0) | number_format }}
+
MOMO 價格優勢
+
{{ overview.momo_threat_count | default(0) | number_format }}
MOMO 價格低於 PChome
@@ -53,12 +54,12 @@
-
可用資料新鮮度
-
{{ '已更新' if overview.last_pchome_crawled else '待更新' }}
+
最新有效價格抓取
+
{{ overview.last_pchome_crawled or '待刷新' }}
- {{ 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 }}
@@ -72,7 +73,7 @@
PCHOME MATCH BACKFILL
PChome 補抓產線
- 待刷新 {{ 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 }}