From e0ed5cc7323b2c8515d2c3739435c3e29a60e932 Mon Sep 17 00:00:00 2001 From: OoO Date: Mon, 1 Jun 2026 13:13:01 +0800 Subject: [PATCH] =?UTF-8?q?V10.552=20=E6=94=B6=E6=96=82=E6=B1=BA=E7=AD=96?= =?UTF-8?q?=E6=9F=A5=E8=A9=A2=E6=96=B0=E9=AE=AE=E5=BA=A6=E5=8F=A3=E5=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TODO_NEXT_STEPS.txt | 1 + config.py | 2 +- docs/memory/history_logs.md | 1 + services/competitor_intel_repository.py | 8 ++++---- tests/test_competitor_intel_cache.py | 7 +++++++ 5 files changed, 14 insertions(+), 5 deletions(-) diff --git a/TODO_NEXT_STEPS.txt b/TODO_NEXT_STEPS.txt index 5ab3e01..9fcb0be 100644 --- a/TODO_NEXT_STEPS.txt +++ b/TODO_NEXT_STEPS.txt @@ -4,6 +4,7 @@ ================================================================================ 【已完成】 + - V10.552 收斂決策查詢的新鮮度口徑:`fetch_top_competitor_risks()`、PChome review queue、review sample 與 current PPT/AI 比價結果都不再把 `expires_at IS NULL` 當成有效現價,只接受 `expires_at > CURRENT_TIMESTAMP` 的 PChome identity_v2 價格。未知新鮮度只留在 coverage 的診斷欄位與 V10.551 刷新入口,不再進入價格風險、簡報、AI 決策或覆核排除條件。 - V10.551 收斂未知新鮮度刷新與補抓排序:`_fetch_expired_identity_skus()` / `_fetch_expired_identity_recovery_skus()` 將 `expires_at IS NULL` 視為必須刷新或可搜尋救援的未知新鮮度 identity,和 V10.549「未知新鮮度不算可決策覆蓋率」口徑對齊;兩條路徑改用 `JOIN LATERAL` 取最新 MOMO 價,移除 product-wide window scan。`_fetch_unmatched_priority_skus()` 也改用 lateral 最新價,並優先重搜低風險 `no_result / refresh_no_result`,讓 V10.550 的安全召回詞先用在最可能被救回的商品。 - V10.550 補安全搜尋召回詞:`_build_variant_recall_search_plan()` 對低風險穩定品類新增 `品牌 + 品類` 的補搜尋詞,讓 `no_result / refresh_no_result` 更有機會找到 PChome 候選後再交給 matcher 安全判斷;美甲片、指甲油、唇彩、香氛/精油、粉底、防曬、任選/色號/款式等高 variant 風險商品不走通用召回,DASHING DIVA 仍只走既有 line-specific recall + sort fallback。此變更不改 `MIN_MATCH_SCORE`、hard veto、fresh-search write safety 或 stronger existing match 覆寫保護。 - 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 價格優勢`,降低誤讀。 diff --git a/config.py b/config.py index c5734f3..099ec47 100644 --- a/config.py +++ b/config.py @@ -402,7 +402,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.551" +SYSTEM_VERSION = "V10.552" 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 d94b8e4..e66c997 100644 --- a/docs/memory/history_logs.md +++ b/docs/memory/history_logs.md @@ -13,6 +13,7 @@ ## 📅 詳細更新日誌 (考古存檔) ### 2026-06-01:PChome 比價新鮮度操作閉環 +- **V10.552 決策查詢新鮮度口徑收斂**: Top competitor risks、PChome review queue、review sample 與 current PPT/AI 比價結果全部改成只吃 `cp.expires_at > CURRENT_TIMESTAMP` 的有效 PChome identity_v2 價格,不再把 `expires_at IS NULL` 當作有效現價。未知新鮮度現在只作 coverage 診斷與刷新入口,不會被用來產生價格風險、簡報、AI 決策或從覆核隊列中排除。 - **V10.551 未知新鮮度刷新與補抓排序收斂**: `expires_at IS NULL` 的 identity_v2 價格現在會被 `_fetch_expired_identity_skus()` 與 `_fetch_expired_identity_recovery_skus()` 視為需要刷新 / 可搜尋救援的未知新鮮度資料,避免 V10.549 已排除出可決策覆蓋率後卻沒有刷新入口;兩條路徑都改用 `JOIN LATERAL` 取最新 MOMO 價,不再做 product-wide window scan。`_fetch_unmatched_priority_skus()` 同步改為 lateral latest price,且把低風險 `no_result / refresh_no_result` 排到補抓前段,讓安全召回詞優先投入最可能回收的 SKU。 - **V10.550 安全搜尋召回詞補強**: `competitor_price_feeder` 在既有精準搜尋詞之外,對低風險穩定品類補上一組 `品牌 + 品類` recall keyword,提升 `no_result / refresh_no_result` 找到候選的機率;高 variant 風險商品如美甲片、指甲油、唇彩、香氛/精油、粉底、防曬與含任選/色號/款式/香味的商品不走通用召回。DASHING DIVA 仍保留既有 line-specific recall 與 PChome sort fallback;本次不更動 `MIN_MATCH_SCORE`、hard veto、auto write safety 或 stronger existing match 保護。 - **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 價格優勢`。 diff --git a/services/competitor_intel_repository.py b/services/competitor_intel_repository.py index a2394ba..982fb5d 100644 --- a/services/competitor_intel_repository.py +++ b/services/competitor_intel_repository.py @@ -1152,7 +1152,7 @@ def _fetch_top_competitor_risks_uncached(engine, limit: int = 10) -> list[dict]: cp.crawled_at FROM competitor_prices cp WHERE cp.source = 'pchome' - AND (cp.expires_at IS NULL OR cp.expires_at > CURRENT_TIMESTAMP) + AND cp.expires_at > CURRENT_TIMESTAMP AND cp.price IS NOT NULL AND cp.price > 0 AND COALESCE(cp.match_score, 0) >= {PCHOME_MATCH_SCORE_FLOOR} @@ -1285,7 +1285,7 @@ def _review_queue_cte_and_filter( FROM competitor_prices cp WHERE cp.source = 'pchome' AND cp.sku = la.sku - AND (cp.expires_at IS NULL OR cp.expires_at > CURRENT_TIMESTAMP) + AND cp.expires_at > CURRENT_TIMESTAMP AND cp.price IS NOT NULL AND cp.price > 0 AND COALESCE(cp.match_score, 0) >= {PCHOME_MATCH_SCORE_FLOOR} @@ -1475,7 +1475,7 @@ def _fetch_competitor_review_queue_uncached(engine, limit: int = 12) -> list[dic cp.sku FROM competitor_prices cp WHERE cp.source = 'pchome' - AND (cp.expires_at IS NULL OR cp.expires_at > CURRENT_TIMESTAMP) + AND cp.expires_at > CURRENT_TIMESTAMP AND cp.price IS NOT NULL AND cp.price > 0 AND COALESCE(cp.match_score, 0) >= {PCHOME_MATCH_SCORE_FLOOR} @@ -1702,7 +1702,7 @@ def fetch_competitor_comparison_results( 'competitor_prices' AS competitor_source FROM competitor_prices cp WHERE cp.source = 'pchome' - AND (cp.expires_at IS NULL OR cp.expires_at > CURRENT_TIMESTAMP) + AND cp.expires_at > CURRENT_TIMESTAMP AND cp.price IS NOT NULL AND cp.price > 0 AND COALESCE(cp.match_score, 0) >= {PCHOME_MATCH_SCORE_FLOOR} diff --git a/tests/test_competitor_intel_cache.py b/tests/test_competitor_intel_cache.py index 259bd9d..f708ab9 100644 --- a/tests/test_competitor_intel_cache.py +++ b/tests/test_competitor_intel_cache.py @@ -99,6 +99,13 @@ def test_competitor_coverage_counts_only_active_product_intersection(): assert "WHERE p.status = 'ACTIVE'" in coverage_source +def test_competitor_decision_consumers_require_explicit_freshness(): + source = (ROOT / "services" / "competitor_intel_repository.py").read_text(encoding="utf-8") + + assert "(cp.expires_at IS NULL OR cp.expires_at > CURRENT_TIMESTAMP)" not in source + assert source.count("AND cp.expires_at > CURRENT_TIMESTAMP") >= 4 + + def test_competitor_ppt_and_ai_use_momo_minus_pchome_gap_direction(): ppt_source = (ROOT / "services" / "ppt_generator.py").read_text(encoding="utf-8") route_source = (ROOT / "routes" / "openclaw_bot_routes.py").read_text(encoding="utf-8")