diff --git a/TODO_NEXT_STEPS.txt b/TODO_NEXT_STEPS.txt index 688cddd..5e34d4c 100644 --- a/TODO_NEXT_STEPS.txt +++ b/TODO_NEXT_STEPS.txt @@ -4,6 +4,7 @@ ================================================================================ 【已完成】 + - V10.317 修正 PChome 比價覆蓋率分子:`fetch_competitor_coverage()` 的 valid_matches 改成 `ACTIVE + 有 MOMO 最新價` 商品與有效 PChome `identity_v2` 價格的交集,不再把非活躍或無 MOMO 現價的舊 competitor_prices 列入覆蓋率,避免 daily/growth/PPT/AI 報表高估比價資料品質。 - V10.315 修正競品簡報/報表指定日期取價:`fetch_competitor_comparison_results()` 在有 start/end date 時改讀 `competitor_price_history` 的期間快照,MOMO 價格也取期間結束前最新價;沒有指定日期才使用目前有效 `competitor_prices`,避免把今天的 PChome 快取價塞回歷史 daily/growth/PPT 判讀。 - V10.314 擴大 PChome 候選池與搜尋韌性:PChome 搜尋 API 改為依 limit 掃多頁並對 429/5xx/timeout 做有限重試;feeder 預設每個商品最多 5 組搜尋詞、每詞 20 候選、2 頁,且搜尋清理不再刪掉括號/方括號內的品牌與規格,讓正確候選更有機會進 matcher,而不是長期停在「待對比」。 - V10.312 強化 PChome 商品身份比對防錯配:matcher 開始解析 mg/mcg 劑量、件組套組與多規格集合,60ml+150ml vs 60ml+20ml、10mg vs 20mg、10片 vs 10盒、精華 vs 化妝水都會進硬否決或單位價覆核,不再靠單一規格重疊放行;覆核診斷同步新增「劑量差異」標籤,降低核心比價錯配污染 daily/growth/PPT/AI 分析。 diff --git a/config.py b/config.py index 7418279..74a738d 100644 --- a/config.py +++ b/config.py @@ -320,7 +320,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.316" +SYSTEM_VERSION = "V10.317" LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log') public_url = PUBLIC_URL # 用於模板顯示 diff --git a/docs/AI_INTELLIGENCE_MODULE_SOT.md b/docs/AI_INTELLIGENCE_MODULE_SOT.md index 171ab2c..441774e 100644 --- a/docs/AI_INTELLIGENCE_MODULE_SOT.md +++ b/docs/AI_INTELLIGENCE_MODULE_SOT.md @@ -608,3 +608,4 @@ POSTGRES_HOST=momo-db | 2026-05-20 | PChome 商品身份比對仍可能因單一規格重疊誤放行 | V10.312 起 matcher 解析 mg/mcg 劑量、件組套組、多規格集合與同數字不同單位;劑量/容量/重量/件數/品類衝突會硬否決或導向單位價覆核,避免錯配污染 Dashboard、daily/growth、PPT 與 AI 競價分析 | | 2026-05-20 | 正確 PChome 候選常因只掃第一頁或搜尋詞丟失品牌/規格而未進入 matcher | V10.314 起搜尋 API 依 limit 掃多頁、對暫時性錯誤有限重試;feeder 預設 5 組搜尋詞、20 候選、2 頁,並保留括號/方括號內品牌與規格,提升覆核隊列與正式比價的候選品質 | | 2026-05-20 | 指定日期競品簡報可能混用目前 `competitor_prices` 快取價 | V10.315 起 `fetch_competitor_comparison_results()` 有 start/end date 時改用 `competitor_price_history` 期間快照,MOMO 價格取報表結束日前最新價;即時報表才使用目前有效 `competitor_prices` | +| 2026-05-20 | PChome 覆蓋率分子可能被非活躍或無 MOMO 現價 SKU 膨脹 | V10.317 起 `fetch_competitor_coverage()` 的 `valid_matches` 改為 active MOMO latest price 與有效 PChome `identity_v2` 價格交集,確保 daily/growth/PPT/AI 看到的比價資料品質不被舊快取列高估 | diff --git a/services/competitor_intel_repository.py b/services/competitor_intel_repository.py index 9d97f6d..7466763 100644 --- a/services/competitor_intel_repository.py +++ b/services/competitor_intel_repository.py @@ -274,7 +274,7 @@ def _cached_payload(cache_key: str, producer, ttl_seconds: int = COMPETITOR_INTE def fetch_competitor_coverage(engine) -> dict: return _cached_payload( - f"coverage:v3:floor={PCHOME_MATCH_SCORE_FLOOR}:manual_reviews=1", + f"coverage:v4:floor={PCHOME_MATCH_SCORE_FLOOR}:manual_reviews=1", lambda: _fetch_competitor_coverage_uncached(engine), ) @@ -349,7 +349,10 @@ def _fetch_competitor_coverage_uncached(engine) -> dict: {attempt_cte} SELECT (SELECT COUNT(*) FROM latest_momo WHERE rn = 1) AS active_with_price, - (SELECT COUNT(*) FROM valid_competitor) AS valid_matches, + (SELECT COUNT(*) + FROM latest_momo lm + JOIN valid_competitor vc ON vc.sku = lm.sku + WHERE lm.rn = 1) AS valid_matches, (SELECT COUNT(*) FROM latest_momo lm LEFT JOIN valid_competitor vc ON vc.sku = lm.sku diff --git a/tests/test_competitor_intel_cache.py b/tests/test_competitor_intel_cache.py index 627f9d1..8e61914 100644 --- a/tests/test_competitor_intel_cache.py +++ b/tests/test_competitor_intel_cache.py @@ -69,6 +69,18 @@ def test_competitor_ppt_results_use_history_for_dated_reports(): assert "指定期間 competitor_price_history" in route_source +def test_competitor_coverage_counts_only_active_product_intersection(): + source = (ROOT / "services" / "competitor_intel_repository.py").read_text(encoding="utf-8") + coverage_source = source.split("def _fetch_competitor_coverage_uncached", 1)[1].split( + "def _fetch_manual_review_summary", 1 + )[0] + + assert "coverage:v4" in source + assert "(SELECT COUNT(*) FROM valid_competitor) AS valid_matches" not in coverage_source + assert "FROM latest_momo lm\n JOIN valid_competitor vc ON vc.sku = lm.sku" in coverage_source + assert "WHERE lm.rn = 1) AS valid_matches" in coverage_source + + 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")