From 82b56169218794f476630f2edfca8cf817ba47c9 Mon Sep 17 00:00:00 2001 From: OoO Date: Sun, 24 May 2026 18:52:57 +0800 Subject: [PATCH] Start PChome review queue from latest attempts --- TODO_NEXT_STEPS.txt | 1 + config.py | 2 +- docs/AI_INTELLIGENCE_MODULE_SOT.md | 2 +- docs/memory/history_logs.md | 1 + services/competitor_intel_repository.py | 69 ++++++++----------- ...t_competitor_match_attempts_persistence.py | 16 +++++ 6 files changed, 49 insertions(+), 42 deletions(-) diff --git a/TODO_NEXT_STEPS.txt b/TODO_NEXT_STEPS.txt index eca8c4b..fa23e23 100644 --- a/TODO_NEXT_STEPS.txt +++ b/TODO_NEXT_STEPS.txt @@ -4,6 +4,7 @@ ================================================================================ 【已完成】 + - V10.447 反轉 PChome 覆核頁查詢方向:review queue page 先從最新 `competitor_match_attempts` 的可覆核狀態縮小候選,再 join ACTIVE 商品與最新價,並用 `NOT EXISTS` 排除已有有效 identity_v2 正式價;避免每次「全部覆核」先掃全站 ACTIVE 商品。 - V10.446 修正 PChome 覆核頁輕量路徑的 overview timeout:覆核頁總覽改讀已存在的 shared dashboard cache / stale cache,沒有快取時只用目前覆核頁資料補足狀態,不再現場跑 `_load_competitor_decision_overview(session)` 的重型後備 SQL。 - V10.445 補 PChome 覆核頁輕量渲染路徑:`filter=pchome_review` 不再先建完整 Dashboard `unique_items`,改為只查覆核當頁 50 筆商品、當前價、昨日價與週前價,再沿用同一張新版表格與人工覆核按鈕;降低核心比價覆核頁的全站資料負載。 - V10.444 瘦身 PChome 覆核頁查詢:`fetch_competitor_review_queue_page()` 將覆核隊列總數與當頁資料合併在單一 SQL 內取回,避免 `/?filter=pchome_review` 為 count/page 重複掃 `latest_momo`、`latest_attempt`、`valid_competitor` CTE;保留狀態分流、人工覆核與正式價格寫入保護不變。 diff --git a/config.py b/config.py index e4fd020..d646722 100644 --- a/config.py +++ b/config.py @@ -325,7 +325,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.446" +SYSTEM_VERSION = "V10.447" 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 96615c4..3fe110a 100644 --- a/docs/AI_INTELLIGENCE_MODULE_SOT.md +++ b/docs/AI_INTELLIGENCE_MODULE_SOT.md @@ -2,7 +2,7 @@ > **最後更新**: 2026-05-24 (台北時間) > **狀態**: 🟢 四 AI Agent 自動化閉環已落地;LLM 路由紅線升級為 Ollama-first 三主機級聯,Gemini 備援預設關閉 -> **適用版本**: V10.446 +> **適用版本**: V10.447 --- diff --git a/docs/memory/history_logs.md b/docs/memory/history_logs.md index ea6f1c3..871f8a7 100644 --- a/docs/memory/history_logs.md +++ b/docs/memory/history_logs.md @@ -13,6 +13,7 @@ ## 📅 詳細更新日誌 (考古存檔) ### 2026-05-24:PChome 近門檻身份回收第二輪 +- **V10.447 PChome 覆核頁查詢方向反轉**: review queue page 改由最新 `competitor_match_attempts` 的可覆核狀態先縮小候選,再 join ACTIVE 商品與最新價,並用 `NOT EXISTS` 排除已有有效 `identity_v2` 正式 PChome 價格;避免「全部覆核」每次先掃全站 ACTIVE 商品後才過濾,提高核心比價覆核頁可操作性。 - **V10.446 PChome 覆核頁 overview timeout 修正**: 覆核頁輕量 render path 的總覽改讀既有 shared dashboard cache / stale cache;若快取不存在,只用目前覆核頁資料補足 review count 與狀態分流,不再即時呼叫 `_load_competitor_decision_overview(session)` 的重型 SQL,避免正式資料量下 statement timeout。 - **V10.445 PChome 覆核頁輕量渲染路徑**: `filter=pchome_review` 直接走覆核隊列專用 render path,不再先建完整 Dashboard `unique_items`;當頁只查 50 筆覆核 SKU 的商品資料、最新價、昨日價與週前價,仍沿用同一張新版表格、狀態分流、PChome 候選說明與人工覆核按鈕,降低核心比價覆核頁的全站資料負載。 - **V10.444 PChome 覆核頁查詢瘦身**: `fetch_competitor_review_queue_page()` 將原本 count + page 兩次重跑 review CTE 改成單次 SQL 的 `total_rows` + `paged_rows` 查詢,同步取得總數與頁面資料,降低 `/?filter=pchome_review` 對 `latest_momo` / `latest_attempt` / `valid_competitor` 的重複掃描;正式站小批次 rescore 入隊後,用於維持核心比價覆核頁可操作速度。 diff --git a/services/competitor_intel_repository.py b/services/competitor_intel_repository.py index 9673907..9235be2 100644 --- a/services/competitor_intel_repository.py +++ b/services/competitor_intel_repository.py @@ -835,48 +835,29 @@ def _review_queue_cte_and_filter( status_values = REVIEW_STATUS_FILTER_GROUPS.get(status_filter) or tuple(ACTIONABLE_ATTEMPT_STATUSES) status_sql = ", ".join(f"'{status}'" for status in status_values) filters = [ - "vc.sku IS NULL", f"la.attempt_status IN ({status_sql})", + f"""NOT EXISTS ( + SELECT 1 + 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.price IS NOT NULL + AND cp.price > 0 + AND COALESCE(cp.match_score, 0) >= {PCHOME_MATCH_SCORE_FLOOR} + AND COALESCE(cp.tags, '[]'::jsonb) ? 'identity_v2' + )""", ] if search_query: params["search_like"] = f"%{search_query.lower()}%" - filters.append("(LOWER(lm.name) LIKE :search_like OR LOWER(lm.sku) LIKE :search_like)") + filters.append("(LOWER(p.name) LIKE :search_like OR LOWER(p.i_code) LIKE :search_like)") if category: params["category"] = category - filters.append("lm.category = :category") + filters.append("p.category = :category") where_sql = "\n AND ".join(filters) cte = f""" - WITH latest_momo AS ( - SELECT - p.id AS product_id, - p.i_code AS sku, - p.name, - p.category, - latest_price.price AS momo_price - FROM products p - JOIN LATERAL ( - SELECT pr.price - FROM price_records pr - WHERE pr.product_id = p.id - ORDER BY pr.timestamp DESC, pr.id DESC - LIMIT 1 - ) latest_price ON TRUE - WHERE p.status = 'ACTIVE' - ), - valid_competitor AS ( - SELECT DISTINCT ON (cp.sku) - cp.sku - FROM competitor_prices cp - WHERE cp.source = 'pchome' - AND (cp.expires_at IS NULL OR 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} - AND COALESCE(cp.tags, '[]'::jsonb) ? 'identity_v2' - ORDER BY cp.sku, cp.crawled_at DESC NULLS LAST - ), - latest_attempt AS ( + WITH latest_attempt AS ( SELECT DISTINCT ON (cma.sku) cma.sku, cma.attempt_status, @@ -894,10 +875,10 @@ def _review_queue_cte_and_filter( ), review_rows AS ( SELECT - lm.sku, - lm.name, - lm.category, - lm.momo_price, + p.i_code AS sku, + p.name, + p.category, + latest_price.price AS momo_price, la.attempt_status, la.candidate_count, la.best_competitor_product_id, @@ -917,9 +898,17 @@ def _review_queue_cte_and_filter( WHEN la.attempt_status = 'expired_match' THEN 6 ELSE 7 END AS priority_rank - FROM latest_momo lm - JOIN latest_attempt la ON la.sku = lm.sku - LEFT JOIN valid_competitor vc ON vc.sku = lm.sku + FROM latest_attempt la + JOIN products p + ON p.i_code = la.sku + AND p.status = 'ACTIVE' + JOIN LATERAL ( + SELECT pr.price + FROM price_records pr + WHERE pr.product_id = p.id + ORDER BY pr.timestamp DESC, pr.id DESC + LIMIT 1 + ) latest_price ON TRUE WHERE {where_sql} ) """ diff --git a/tests/test_competitor_match_attempts_persistence.py b/tests/test_competitor_match_attempts_persistence.py index a20fde8..7d2a564 100644 --- a/tests/test_competitor_match_attempts_persistence.py +++ b/tests/test_competitor_match_attempts_persistence.py @@ -55,6 +55,22 @@ def test_competitor_review_queue_page_uses_single_paged_total_query(): assert "if row.get(\"sku\")" in page_body +def test_competitor_review_queue_starts_from_latest_attempts_not_all_products(): + source = (ROOT / "services/competitor_intel_repository.py").read_text(encoding="utf-8") + review_cte_body = _function_body( + source, + "_review_queue_cte_and_filter", + "_fetch_competitor_review_queue_page_uncached", + ) + + assert "FROM latest_attempt la" in review_cte_body + assert "JOIN products p" in review_cte_body + assert "JOIN LATERAL" in review_cte_body + assert "NOT EXISTS (" in review_cte_body + assert "FROM latest_momo lm" not in review_cte_body + assert "valid_competitor AS" not in review_cte_body + + def test_competitor_feeder_persists_all_match_attempt_outcomes(): source = (ROOT / "services/competitor_price_feeder.py").read_text(encoding="utf-8") migration = (ROOT / "migrations/023_competitor_match_attempts.sql").read_text(encoding="utf-8")