diff --git a/TODO_NEXT_STEPS.txt b/TODO_NEXT_STEPS.txt index b2bb136..85e1d26 100644 --- a/TODO_NEXT_STEPS.txt +++ b/TODO_NEXT_STEPS.txt @@ -4,6 +4,7 @@ ================================================================================ 【已完成】 + - V10.444 瘦身 PChome 覆核頁查詢:`fetch_competitor_review_queue_page()` 將覆核隊列總數與當頁資料合併在單一 SQL 內取回,避免 `/?filter=pchome_review` 為 count/page 重複掃 `latest_momo`、`latest_attempt`、`valid_competitor` CTE;保留狀態分流、人工覆核與正式價格寫入保護不變。 - V10.443 補 PChome rescore 人工覆核入隊:`audit_competitor_match_attempt_rescore.py --apply-accepted` 只追加 `rescore_accepted_current` attempt 進人工覆核隊列,不直接寫 `competitor_prices` / `competitor_price_history`;商品看板新增「重算可採用」分流與狀態文案,讓可救回候選先由人審確認再正式更新價差。 - V10.442 降噪 `/cicd` 舊 GitLab 探測:沒有明確啟用 `GITLAB_ENABLED=true` 與 token 時,不再打退役的 `192.168.0.110:8929` 或 SSH fallback,正式 responsive smoke 造訪 `/cicd` 只呈現空 pipeline 狀態,不污染 app logs。 - V10.441 補 PChome matcher re-score audit 與商品看板原因標籤:新增 read-only `competitor_match_attempt_rescore_audit` / `scripts/audit_competitor_match_attempt_rescore.py`,可用最新版 matcher 重新分類既有 `competitor_match_attempts`,預設不寫 DB、不更新正式價格;商品看板同步補蘭蔻/達特醫/hoi/Saugella/Lactacyd 等 focused matcher reason 中文標籤,讓「待對比」能拆成商品線不符、款式版本不符、可回刷或仍低信心。 diff --git a/config.py b/config.py index aec4ca8..8bb8a1c 100644 --- a/config.py +++ b/config.py @@ -325,7 +325,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.443" +SYSTEM_VERSION = "V10.444" 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 de4d274..e152ce8 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.443 +> **適用版本**: V10.444 --- diff --git a/docs/memory/history_logs.md b/docs/memory/history_logs.md index 0fa1d80..c10eaba 100644 --- a/docs/memory/history_logs.md +++ b/docs/memory/history_logs.md @@ -13,6 +13,7 @@ ## 📅 詳細更新日誌 (考古存檔) ### 2026-05-24: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 入隊後,用於維持核心比價覆核頁可操作速度。 - **V10.443 PChome rescore 人工覆核入隊**: `scripts/audit_competitor_match_attempt_rescore.py --apply-accepted` 只會把最新版 matcher 已通過門檻的舊低信心候選追加為 `rescore_accepted_current` attempt,進入商品看板人工覆核隊列;不寫 `competitor_prices`、不寫 `competitor_price_history`,必須由操作員按「採用同款」後才正式更新 PChome 價差。Dashboard 補上「重算可採用待審」分流與狀態文案,避免安全回刷候選混在一般低信心項目裡。 - **V10.442 CI/CD legacy GitLab 探測降噪**: `/cicd` 舊 GitLab pipeline API 預設改為 disabled,除非明確設定 `GITLAB_ENABLED=true` 並提供 `GITLAB_TOKEN`,否則不再打 `192.168.0.110:8929` 或 SSH fallback;正式 responsive smoke 造訪 `/cicd` 時只呈現可診斷空狀態,不再把已退役 GitLab endpoint 的 connection refused / permission denied 寫成錯誤噪音。 - **V10.441 PChome matcher re-score audit**: 新增 read-only `competitor_match_attempt_rescore_audit` 服務與 `scripts/audit_competitor_match_attempt_rescore.py`,可針對既有 `competitor_match_attempts` 用最新版 matcher 重新分類成 `accepted_current` / `unit_comparable_current` / `identity_veto_current` / `low_score_current`,預設不寫 DB、不更新正式價格;商品看板同步補蘭蔻/達特醫/hoi/Saugella/Lactacyd 等 focused matcher reason 中文標籤。正式抽樣中 31 筆舊 `strong_exact_spec_match` 低信心資料,最新版 matcher 可讀出 10 筆 gate pass、1 筆單位價、11 筆 hard veto、9 筆仍低信心,作為後續人工覆核與批次回刷前的安全量化。 diff --git a/services/competitor_intel_repository.py b/services/competitor_intel_repository.py index 8f87901..9673907 100644 --- a/services/competitor_intel_repository.py +++ b/services/competitor_intel_repository.py @@ -961,24 +961,33 @@ def _fetch_competitor_review_queue_page_uncached( "limit": per_page, "offset": (page - 1) * per_page, } - count_sql = text(cte + " SELECT COUNT(*) AS total FROM review_rows") page_sql = text(cte + """ - SELECT * - FROM review_rows - ORDER BY - priority_rank ASC, - momo_price DESC NULLS LAST, - best_match_score DESC NULLS LAST, - attempted_at DESC NULLS LAST - LIMIT :limit OFFSET :offset + , total_rows AS ( + SELECT COUNT(*) AS total_count + FROM review_rows + ), + paged_rows AS ( + SELECT * + FROM review_rows + ORDER BY + priority_rank ASC, + momo_price DESC NULLS LAST, + best_match_score DESC NULLS LAST, + attempted_at DESC NULLS LAST + LIMIT :limit OFFSET :offset + ) + SELECT paged_rows.*, total_rows.total_count + FROM total_rows + LEFT JOIN paged_rows ON TRUE """) with engine.connect() as conn: - total = int(conn.execute(count_sql, params).scalar() or 0) rows = conn.execute(page_sql, page_params).mappings().all() + total = int(rows[0].get("total_count") or 0) if rows else 0 + item_rows = [dict(row) for row in rows if row.get("sku")] return { - "items": [_format_competitor_review_item(dict(row)) for row in rows], + "items": [_format_competitor_review_item(row) for row in item_rows], "total": total, "page": page, "per_page": per_page, diff --git a/tests/test_competitor_match_attempts_persistence.py b/tests/test_competitor_match_attempts_persistence.py index 7c958aa..a20fde8 100644 --- a/tests/test_competitor_match_attempts_persistence.py +++ b/tests/test_competitor_match_attempts_persistence.py @@ -39,6 +39,22 @@ def test_competitor_dashboard_hot_paths_use_latest_price_lateral_lookup(): assert "lm.rn = 1" not in body +def test_competitor_review_queue_page_uses_single_paged_total_query(): + source = (ROOT / "services/competitor_intel_repository.py").read_text(encoding="utf-8") + page_body = _function_body( + source, + "_fetch_competitor_review_queue_page_uncached", + "_fetch_competitor_review_queue_uncached", + ) + + assert "total_rows AS" in page_body + assert "paged_rows AS" in page_body + assert "LEFT JOIN paged_rows ON TRUE" in page_body + assert "SELECT COUNT(*) AS total FROM review_rows" not in page_body + assert "rows[0].get(\"total_count\")" in page_body + assert "if row.get(\"sku\")" in page_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")