V10.521 顯示比價 stale 新鮮度指標
All checks were successful
CD Pipeline / deploy (push) Successful in 1m7s

This commit is contained in:
OoO
2026-05-31 23:49:43 +08:00
parent dd6b6160fe
commit bf2888b0fb
11 changed files with 36 additions and 3 deletions

View File

@@ -4,6 +4,7 @@
================================================================================
【已完成】
- V10.521 將比價新鮮度 stale 指標上屏:首頁 KPI / PChome 補抓產線 / daily / growth 都顯示價格過期數,讓操作員分清「已確認同款但價格待刷新」與「尚未找到身份配對」;過期 identity refresh 也優先刷新 `total_price / price_alert_exact` 的正式價差配對。
- V10.520 拆開過期價格刷新與搜尋救援:`run_expired_identity_refresh()` 只刷新既有 `identity_v2` PChome product_id不再因少數 product_id 查不到或低分而同步進入慢速 `fresh_search_recovery`;缺失 / 低分候選交給 `run_retryable_candidate_revalidation()` 處理,避免正式刷新 500+ 筆時被外部搜尋拖死,讓價格新鮮度可以穩定批次回升。
- V10.519 對齊 Webcrumbs host data metadata 與新版比價覆蓋口徑:`services/webcrumbs_host_data_service.py` 會同時輸出身份覆蓋、價格新鮮、過期配對與待補抓數,讓 shared-ui plugin / 其他專案 proxy 不會把 `coverage_rate` 誤讀成價格可用率。
- V10.518 修正 PChome 比價覆蓋率口徑與新鮮度產線:`fetch_competitor_coverage()` 改拆「身份覆蓋」與「價格新鮮」,覆蓋率不再因 `expires_at` 過期被歸零;首頁 / 業績 / 成長頁同步顯示身份覆蓋、價格新鮮數與新鮮率。PChome 快取 TTL 預設由 6h 改 48h並把每日 expired identity refresh / retryable / unmatched limits 改為環境變數,預設提高到 1200 / 240 / 240避免 1800+ 已配對 identity 因刷新量不足長期失效。

View File

@@ -402,7 +402,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
# ==========================================
# 系統版本與路徑
# ==========================================
SYSTEM_VERSION = "V10.520"
SYSTEM_VERSION = "V10.521"
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
public_url = PUBLIC_URL # 用於模板顯示

View File

@@ -390,6 +390,7 @@ LEFT JOIN competitor_prices cp
- competitor PPT 不可只輸出 matched rows 造成覆蓋率假象;`fetch_competitor_comparison_results()` 必須用 `LEFT JOIN valid_competitor` 保留高營收/高價但尚未有效配對的 MOMO 商品,並帶出 `match_status``candidate_count``best_match_score``match_diagnostic`,讓簡報與 AI 文案明確區分「高信心比對」與「待補身份/價格」。
- `services/competitor_identity_revalidator.py` 可對既有 `competitor_prices` legacy row 離線重跑 `identity_v2`:只有新版 matcher 分數 `>= 0.76` 且無 hard veto 才補 `identity_v2` / `legacy_revalidated` tags預設不刷新 `expires_at`,避免過期價格進入決策。
- `CompetitorPriceFeeder.run_expired_identity_refresh()` 會優先刷新已通過 `identity_v2` 但 TTL 過期的 PChome row直接用既有 `competitor_product_id` 批次呼叫 PChome 商品 API再用新版 matcher 重新驗證名稱/規格/價格 sanity通過後寫回 `competitor_prices``competitor_price_history`。這條路徑提升新鮮價格覆蓋率,但不降低 match threshold也不讓過期價格直接進入決策若既有 `competitor_product_id` 已查不到或回傳候選低於門檻expired refresh 只寫 `refresh_no_result` / 低信心 attempt 並標記 `fresh_search_recovery_deferred`,不得在同一條價格刷新路徑 fresh search 替換正式 identity。fresh search recovery 只保留給 retryable candidate revalidation / unmatched priority 等補抓路徑。
- 過期 identity refresh 排序必須優先 `price_basis_total_price` / `alert_tier_price_alert_exact``match_diagnostic_json.price_basis='total_price'` / `alert_tier='price_alert_exact'` 的正式價差配對,再依 `expires_at` 與 MOMO 價格排序,避免高風險可決策價差長期排在低價或非告警型 stale row 後面。
- `marketplace_product_matcher.py` 的擴充只能走「正向證據 + 反向 veto」品牌一致、商品線/型號訊號強、價格合理且無 hard veto 時才允許 `strong_product_line_match` 加分;補充瓶/補充包/refill 與一般正裝不互相配對,分享組/加量組/明星組等組合包不得誤配單品。
- 近門檻規則必須成對補「召回 + 防錯配」測試:可召回者需有品牌、商品線、規格或具名 identity anchor例如 MUJI 精油芬香護手霜、Mustela 慕之幼爽身潤膚乳、Herbacin 小甘菊護手霜;防錯配者需成為 hard veto例如 M·A·C Macximal 柔霧/緞光唇膏質地、ERBE 指甲清垢棒/指甲緣刨刀功能、Schick 舒芙/舒綺女用除毛刀品線。不得用單一同規格或同品牌放寬全域門檻。
- 套組/買送/件數不同但品牌、核心商品線與單一基礎規格一致時matcher 必須回傳 `comparison_mode='unit_comparable'``unit_comparable` reasonFeeder 只能寫入 `competitor_match_attempts.attempt_status='unit_comparable'``refresh_unit_comparable`,不得寫入 `competitor_prices`。Dashboard 與 `competitor_intel_repository` 必須用 `build_unit_price_comparison()` 產生每 ml / 每 g / 每入單位價證據,讓 PPT / AI 報表可說明「需單位價比較」而不是把總價當同款價差。商品看板在正式配對尚未成立時,仍必須顯示最佳候選 PChome 商品名稱、候選價與「候選價需單位換算」說明讓人工覆核可直接看見下一步daily/growth、PPT 與 OpenClaw 摘要不得自建查詢,需消費 `fetch_competitor_review_queue()` 與 coverage 的 `unit_comparable_count`。若任一側含多個不同容量/重量規格,視為多品項套組,不可進 `unit_comparable`

View File

@@ -13,6 +13,7 @@
## 📅 詳細更新日誌 (考古存檔)
### 2026-05-31Webcrumbs 共用 UI Runtime 與市場情報 writer approval
- **V10.521 比價新鮮度 stale 指標上屏**: 首頁比價監控總覽、PChome 補抓產線、daily 競價覆蓋與 growth 比價資料品質同步顯示 `stale_matches` / 價格過期數,讓操作員能分清「已確認同款但價格待刷新」與「尚未找到身份配對」,不再只看到新鮮率下降。過期 identity refresh 也優先刷新 `total_price / price_alert_exact` 的正式價差配對,讓最能進決策與告警的舊價格先回新鮮。
- **V10.520 PChome 過期價格刷新快慢路徑拆分**: `run_expired_identity_refresh()` 改為只刷新已確認 `identity_v2` 的既有 PChome product_id若 product_id 已查不到或回傳後低分,不再同步跑慢速 fresh search recovery而是記錄 `refresh_no_result` / low-score 並交給 `run_retryable_candidate_revalidation()` 的近門檻救援路徑。這能避免正式回刷 500+ 筆時被少數缺失 ID 拖到長時間卡住,讓價格新鮮度批次回升更可控。
- **V10.519 Webcrumbs host data metadata 對齊新覆蓋率口徑**: Webcrumbs host data metadata 同步輸出 `fresh_match_count``fresh_match_rate``stale_match_count``pending_match_count`,讓共用 UI / 其他專案 proxy 能分清身份覆蓋與價格新鮮度,不再只看到舊的 matched_count / coverage_rate。
- **V10.518 PChome 覆蓋率與新鮮度拆分**: 修正比價監控總覽把價格 TTL 過期誤算成「未覆蓋」的產品口徑,`fetch_competitor_coverage()` 現在分開回報 `valid_matches`identity 覆蓋)、`fresh_matches`(價格新鮮)、`stale_matches``fresh_match_rate`首頁、業績與成長頁同步顯示身份覆蓋與價格新鮮。PChome 快取 TTL 預設由 6h 改 48h並將每日 expired identity refresh / retryable / unmatched limits 改為環境變數,預設提升到 1200 / 240 / 240避免已建立 identity 的商品因刷新量不足被長期視為無覆蓋。

View File

@@ -1168,7 +1168,22 @@ class CompetitorPriceFeeder:
AND COALESCE(cp.match_score, 0) >= :match_score_floor
AND COALESCE(cp.tags, '[]'::jsonb) ? 'identity_v2'
WHERE lm.rn = 1
ORDER BY cp.expires_at ASC, lm.momo_price DESC NULLS LAST, lm.sku
ORDER BY
CASE
WHEN (
COALESCE(cp.tags, '[]'::jsonb) ? 'price_basis_total_price'
OR cp.match_diagnostic_json->>'price_basis' = 'total_price'
)
AND (
COALESCE(cp.tags, '[]'::jsonb) ? 'alert_tier_price_alert_exact'
OR cp.match_diagnostic_json->>'alert_tier' = 'price_alert_exact'
)
THEN 0
ELSE 1
END,
cp.expires_at ASC,
lm.momo_price DESC NULLS LAST,
lm.sku
LIMIT :limit
""")
with self.engine.connect() as conn:

View File

@@ -355,6 +355,10 @@
<span>價格新鮮</span>
<strong class="momo-mono">{{ comp_coverage.fresh_matches | default(0) | number_format }}</strong>
</div>
<div>
<span>價格過期</span>
<strong class="momo-mono">{{ comp_coverage.stale_matches | default(0) | number_format }}</strong>
</div>
<div>
<span>待審/待補</span>
<strong class="momo-mono">{{ comp_coverage.pending | default(0) | number_format }}</strong>

View File

@@ -22,6 +22,7 @@
<div class="dashboard-kpi-sub momo-mono">
{{ overview.matched_count | default(0) | number_format }} / {{ overview.total_active | default(total_products) | number_format }} ACTIVE
· 新鮮 {{ overview.fresh_match_count | default(0) | number_format }}
· 過期 {{ overview.stale_match_count | default(0) | number_format }}
</div>
</div>
<div class="dashboard-kpi is-accent">
@@ -56,6 +57,7 @@
<div class="dashboard-kpi-sub momo-mono">
{{ overview.last_pchome_crawled or '尚無 PChome 抓取紀錄' }}
· 新鮮率 {{ overview.fresh_match_rate | default(0) }}%
· 待刷新 {{ overview.stale_match_count | default(0) | number_format }}
</div>
</div>
</div>
@@ -68,7 +70,7 @@
<div class="dashboard-backfill-label momo-mono">PCHOME MATCH BACKFILL</div>
<div class="dashboard-backfill-title">PChome 補抓產線</div>
<div class="dashboard-backfill-meta momo-mono">
待補抓 {{ overview.pending_match_count | default(0) | number_format }} · 覆核 {{ overview.review_queue_count | default(0) | number_format }} · 重算可採用 {{ overview.rescore_accepted_count | default(0) | number_format }} · 單位價 {{ overview.unit_comparable_count | default(0) | number_format }}
刷新 {{ overview.stale_match_count | default(0) | number_format }} · 待補抓 {{ overview.pending_match_count | default(0) | number_format }} · 覆核 {{ overview.review_queue_count | default(0) | number_format }} · 重算可採用 {{ overview.rescore_accepted_count | default(0) | number_format }} · 單位價 {{ overview.unit_comparable_count | default(0) | number_format }}
</div>
</div>
<div class="dashboard-backfill-progress" aria-hidden="true">

View File

@@ -149,6 +149,8 @@
<strong class="momo-mono">{{ coverage.fresh_matches | default(0) | number_format }}</strong>
<span>新鮮率</span>
<strong class="momo-mono">{{ coverage.fresh_match_rate | default(0) }}%</strong>
<span>價格過期</span>
<strong class="momo-mono">{{ coverage.stale_matches | default(0) | number_format }}</strong>
<span>待審/待補</span>
<strong class="momo-mono">{{ coverage.pending | default(0) | number_format }}</strong>
<span>需單位價覆核</span>

View File

@@ -127,12 +127,14 @@ def test_competitor_review_queue_is_canonical_unit_price_handoff():
assert "competitor_intel.review_queue" in daily_template
assert "coverage.fresh_matches" in growth_template
assert "coverage.fresh_match_rate" in growth_template
assert "coverage.stale_matches" in growth_template
assert "coverage.unit_comparable_count" in growth_template
assert "coverage.rescore_accepted_count" in growth_template
assert "coverage.manual_accept_count" in growth_template
assert "coverage.manual_reject_count" in growth_template
assert "coverage.manual_unit_price_count" in growth_template
assert "comp_coverage.rescore_accepted_count" in daily_template
assert "comp_coverage.stale_matches" in daily_template
def test_competitor_review_filters_split_low_score_operational_buckets():

View File

@@ -103,6 +103,8 @@ def test_competitor_feeder_persists_all_match_attempt_outcomes():
assert "_fetch_expired_identity_skus" in source
assert "run_expired_identity_refresh" in source
assert "allow_missing_recovery=False" in source
assert "price_basis_total_price" in source
assert "alert_tier_price_alert_exact" in source
assert "_fetch_retryable_candidate_skus" in source
assert "run_retryable_candidate_revalidation" in source
retryable_source = source.split("def _fetch_retryable_candidate_skus", 1)[1].split(

View File

@@ -157,6 +157,7 @@ def test_dashboard_v2_is_production_default_and_uses_real_dashboard_data():
assert "competitor_map = _load_pchome_competitor_map(session, item_map.keys())" in route_source
assert "ai_price_recommendations" in route_source
assert "pending_match_count" in route_source
assert "stale_match_count" in route_source
assert "review_queue_count" in route_source
assert "unit_comparable_count" in route_source
assert "rescore_accepted_count" in route_source
@@ -180,6 +181,8 @@ def test_dashboard_v2_is_production_default_and_uses_real_dashboard_data():
assert "比價監控總覽" in dashboard
assert "比價決策焦點" in dashboard
assert "overview.match_rate" in dashboard
assert "overview.stale_match_count" in dashboard
assert "待刷新 {{ overview.stale_match_count" in dashboard
assert "overview.top_picks" in dashboard
assert "overview.top_momo_threats" in dashboard
assert "overview.pending_priority" in dashboard