From e5ecf5512e0f44b8942c7ed5fd91279304e98aa1 Mon Sep 17 00:00:00 2001 From: OoO Date: Mon, 1 Jun 2026 12:09:29 +0800 Subject: [PATCH] =?UTF-8?q?V10.546=20=E8=A3=9C=E8=BF=91=E9=96=80=E6=AA=BB?= =?UTF-8?q?=E8=88=8A=E5=80=99=E9=81=B8=E5=9B=9E=E5=88=B7?= 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_price_feeder.py | 39 ++++++++++++++++++- ...t_competitor_match_attempts_persistence.py | 10 +++++ 5 files changed, 50 insertions(+), 3 deletions(-) diff --git a/TODO_NEXT_STEPS.txt b/TODO_NEXT_STEPS.txt index 1bb6a7f..64e6013 100644 --- a/TODO_NEXT_STEPS.txt +++ b/TODO_NEXT_STEPS.txt @@ -4,6 +4,7 @@ ================================================================================ 【已完成】 + - 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。 - V10.545 收斂 Dashboard 比價覆蓋率口徑:coverage cache 升到 v9,新增身份覆蓋、可用比價、新鮮度、待補身份、過期身份與人工閉環欄位;商品看板和 PChome 覆核頁改只把真正待處理狀態算進「比價覆核」,人工已否決 / 人工單位價 / 需補研究改列為人工閉環;PChome competitor map 只吃有效價格、新鮮、identity_v2 最新 row,資料新鮮度也改看可用比價 row。 - V10.544 收斂變體安全與 YES 指甲工具線:新增 YES 德悅氏指甲剪附除垢銼刀、腳皮銼腳板、藍寶石銼刀、三面拋光棒與 6/8cm 指甲剪的精準 total-price 線,要求同品牌、同工具名稱、同尺寸與同亮面/霧面/可收納/三面/不掉屑等款式訊號;同步接進 revalidation SQL。新增 MUJI / COCODOR 未知香味差異與 OPI 無型號不同色名 hard veto,HOOOME 暖燈材質差留人工覆核,搜尋詞也會優先帶香味/色名,提升 crawler 精準候選率。 - V10.543 打通 `rescore_accepted_current` 窄門回刷:已進人工覆核池的候選若命中具名 focused exact 線,可進 `run_retryable_candidate_revalidation()` 重新評分;新增 SK-II 青春露 330ml 兩入、AMIINO 安美諾 30ml、YES 腳指甲剪刀 10.5cm、YES 極細指甲緣硬皮剪刀 9cm 的安全 total-price 線,並補 ANNY / OPI 指甲油型號 code hard veto,避免不同色號被錯配。 diff --git a/config.py b/config.py index adc9e90..3ec2451 100644 --- a/config.py +++ b/config.py @@ -402,7 +402,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.545" +SYSTEM_VERSION = "V10.546" 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 9c03c58..8a428a5 100644 --- a/docs/memory/history_logs.md +++ b/docs/memory/history_logs.md @@ -13,6 +13,7 @@ ## 📅 詳細更新日誌 (考古存檔) ### 2026-06-01:PChome 比價新鮮度操作閉環 +- **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,避免為了覆蓋率破壞安全邊界。 - **V10.545 Dashboard 比價覆蓋率口徑收斂**: 商品看板與 PChome 覆核頁把「身份覆蓋率」與「可用比價覆蓋率」拆成明確欄位,coverage cache 更新為 v9 並回傳 `identity_coverage_*`、`pending_identity_count`、`stale_identity_count`、`last_decision_ready_crawled_at`。覆核 KPI 改只計算真正待處理狀態,人工否決 / 人工單位價 / 需補研究另列 `manual_closed_count`,避免人工閉環候選被混進待審總數。Dashboard 的 PChome competitor map 也改成只取新鮮、有效價格、identity_v2 的最新 row,資料新鮮度改看可用比價 row,不再被無效或低信心抓取紀錄撐高。 - **V10.544 變體安全與 YES 工具線收斂**: 延續近門檻 `low_score` 救回,但把安全邊界補得更硬。新增 YES 德悅氏指甲工具精準線,只有同品牌、同工具線、同尺寸且同亮面/霧面/可收納/三面等關鍵款式時才進 `total_price`,並接入 revalidation SQL。同步新增未知香味差異與無型號指彩色名差異 hard veto:MUJI / COCODOR 不同香型、OPI 無型號不同色名不再被高分誤配;HOOOME 暖燈陶瓷/玻璃/水晶/金屬等材質差保留人工覆核。搜尋詞對護手霜、擴香瓶、無型號指彩優先帶上香味/色名,提升 crawler 找到真同款候選的機率。 - **V10.543 rescore accepted 窄門回刷與高信心線補強**: `run_retryable_candidate_revalidation()` 追加 `rescore_accepted_current` 窄門,只允許已進人工池且命中具名 focused exact 品線的候選重新評分,仍由 matcher 判定是否可寫正式價差。新增 SK-II 青春露 330ml 兩入、AMIINO 安美諾美白修護霜 30ml、YES 腳指甲剪刀 10.5cm、YES 極細指甲緣硬皮剪刀 9cm 的 total-price 安全線;同時新增指甲油型號衝突防線,ANNY `A10.074.60` vs `A10.500`、OPI 不同 `ISL...` 型號都會 hard veto,不會為了拉覆蓋率誤配色號。 diff --git a/services/competitor_price_feeder.py b/services/competitor_price_feeder.py index c1b53a0..27c2bde 100644 --- a/services/competitor_price_feeder.py +++ b/services/competitor_price_feeder.py @@ -1159,14 +1159,49 @@ class CompetitorPriceFeeder: WHERE cma.source = 'pchome' ORDER BY cma.sku, cma.attempted_at DESC, cma.id DESC ), + legacy_unmasked_attempt AS ( + SELECT DISTINCT ON (cma.sku, cma.best_competitor_product_id) + cma.sku, + cma.best_competitor_product_id, + cma.best_competitor_product_name, + cma.best_match_score, + cma.attempt_status, + cma.hard_veto, + cma.match_diagnostic_json, + cma.attempted_at + FROM competitor_match_attempts cma + JOIN latest_attempt current_la + ON current_la.sku = cma.sku + WHERE cma.source = 'pchome' + AND current_la.attempt_status IN ( + 'refresh_no_result', + 'no_result', + 'expired_match' + ) + AND cma.attempt_status IN ( + 'low_score', + 'refresh_low_score', + 'recoverable_low_score', + 'true_low_confidence', + 'rescore_accepted_current' + ) + AND cma.attempted_at < current_la.attempted_at + ORDER BY cma.sku, cma.best_competitor_product_id, cma.attempted_at DESC, cma.id DESC + ), candidate_attempt AS ( - SELECT la.* - FROM latest_attempt la + SELECT DISTINCT ON (la.sku, la.best_competitor_product_id) + la.* + FROM ( + SELECT * FROM latest_attempt + UNION ALL + SELECT * FROM legacy_unmasked_attempt + ) la WHERE la.best_competitor_product_id IS NOT NULL AND la.best_competitor_product_id <> '' AND COALESCE(la.best_match_score, 0) >= :min_score AND COALESCE(la.hard_veto, false) = false AND COALESCE(la.match_diagnostic_json->>'comparison_mode', 'exact_identity') = 'exact_identity' + ORDER BY la.sku, la.best_competitor_product_id, la.attempted_at DESC ) SELECT p.id AS product_id, diff --git a/tests/test_competitor_match_attempts_persistence.py b/tests/test_competitor_match_attempts_persistence.py index 153e12f..a9aaa28 100644 --- a/tests/test_competitor_match_attempts_persistence.py +++ b/tests/test_competitor_match_attempts_persistence.py @@ -177,6 +177,16 @@ def test_competitor_feeder_persists_all_match_attempt_outcomes(): assert "match_diagnostic_json->>'comparison_mode'" in retryable_source assert "?| array[" in retryable_source assert "candidate_attempt AS" in retryable_source + assert "legacy_unmasked_attempt AS" in retryable_source + assert "JOIN latest_attempt current_la" in retryable_source + assert "current_la.attempt_status IN (" in retryable_source + assert "'refresh_no_result'" in retryable_source + assert "'expired_match'" in retryable_source + assert "cma.attempt_status IN (" in retryable_source + assert "cma.attempted_at < current_la.attempted_at" in retryable_source + assert "UNION ALL" in retryable_source + assert "SELECT * FROM legacy_unmasked_attempt" in retryable_source + assert "SELECT DISTINCT ON (la.sku, la.best_competitor_product_id)" in retryable_source assert "FROM candidate_attempt la" in retryable_source assert "JOIN LATERAL" in retryable_source assert "ORDER BY pr.timestamp DESC, pr.id DESC" in retryable_source