This commit is contained in:
@@ -344,6 +344,7 @@ LEFT JOIN competitor_prices cp
|
||||
- `CompetitorPriceFeeder.run_expired_identity_refresh()` 會優先刷新已通過 `identity_v2` 但 TTL 過期的 PChome row:直接用既有 `competitor_product_id` 批次呼叫 PChome 商品 API,再用新版 matcher 重新驗證名稱/規格/價格 sanity,通過後寫回 `competitor_prices` 與 `competitor_price_history`。這條路徑提升新鮮價格覆蓋率,但不降低 match threshold,也不讓過期價格直接進入決策。
|
||||
- `marketplace_product_matcher.py` 的擴充只能走「正向證據 + 反向 veto」:品牌一致、商品線/型號訊號強、價格合理且無 hard veto 時才允許 `strong_product_line_match` 加分;補充瓶/補充包/refill 與一般正裝不互相配對,分享組/加量組/明星組等組合包不得誤配單品。
|
||||
- PChome feeder 的外部 request timeout 由 `PCHOME_FEEDER_TIMEOUT` 控制,預設 12 秒;排程不得因單一 PChome 搜尋 API timeout 被拖到數分鐘。
|
||||
- 商品看板的 PChome 狀態必須把 matcher 診斷原因翻成可行動語意:品牌衝突、規格衝突、補充包差異、組合差異、商品線不符等,不可只顯示籠統「待比對」或「身份否決」。
|
||||
- Dashboard 必須把「待比對」拆成可診斷狀態:`價格過期待刷新`、`舊版配對待重驗`、`低分配對待審`、`身份否決`、`找不到同款`、`抓取異常`、`尚未搜尋`。不可再用單一「待比對」掩蓋資料品質原因。
|
||||
|
||||
### 執行方式
|
||||
|
||||
@@ -62,6 +62,24 @@ def _to_float(value):
|
||||
return None
|
||||
|
||||
|
||||
def _diagnostic_match_rejection_label(diagnostic_text, score_text, *, blocked=True):
|
||||
diagnostic_text = diagnostic_text or ''
|
||||
suffix = '已停止自動採用' if blocked else '不自動採用以避免錯配'
|
||||
if 'refill_pack_conflict' in diagnostic_text:
|
||||
return '補充包差異待審', f'{score_text},補充瓶/補充包與一般正裝不同,{suffix}'
|
||||
if any(token in diagnostic_text for token in ('bundle_offer_conflict', 'multi_component_conflict')):
|
||||
return '組合差異待審', f'{score_text},組合包/多件組與單品不同,{suffix}'
|
||||
if 'brand_conflict' in diagnostic_text:
|
||||
return '品牌衝突待審', f'{score_text},品牌不一致,{suffix}'
|
||||
if any(token in diagnostic_text for token in ('volume_conflict', 'weight_conflict', 'count_conflict', 'component_count_conflict')):
|
||||
return '規格衝突待審', f'{score_text},容量/件數不一致,{suffix}'
|
||||
if 'type_conflict' in diagnostic_text:
|
||||
return '品類衝突待審', f'{score_text},品類不一致,{suffix}'
|
||||
if 'product_line_conflict' in diagnostic_text:
|
||||
return '商品線不符待審', f'{score_text},商品線訊號不足,{suffix}'
|
||||
return '身份否決' if blocked else '低信心待審', f'{score_text},{suffix}'
|
||||
|
||||
|
||||
def _build_pchome_match_status(attempt=None, ineligible=None):
|
||||
if attempt:
|
||||
status = attempt.get('attempt_status') or 'unknown'
|
||||
@@ -86,10 +104,15 @@ def _build_pchome_match_status(attempt=None, ineligible=None):
|
||||
if status == 'identity_veto':
|
||||
score = _to_float(attempt.get('best_match_score'))
|
||||
score_text = f"最佳候選 {round(score * 100)}%" if score is not None else "已拒絕候選"
|
||||
label, summary = _diagnostic_match_rejection_label(
|
||||
attempt.get('error_message'),
|
||||
score_text,
|
||||
blocked=True,
|
||||
)
|
||||
return {
|
||||
'label': '身份否決',
|
||||
'label': label,
|
||||
'tone': 'neutral',
|
||||
'summary': '新版 identity_v2 判定不是同款,已阻擋自動比價',
|
||||
'summary': summary,
|
||||
'detail': score_text,
|
||||
}
|
||||
|
||||
@@ -141,18 +164,11 @@ def _build_pchome_match_status(attempt=None, ineligible=None):
|
||||
|
||||
if status == 'low_score':
|
||||
diagnostic_text = attempt.get('error_message') or ''
|
||||
if 'brand_conflict' in diagnostic_text:
|
||||
label = '品牌衝突待審'
|
||||
summary = f'{score_text},品牌不一致,已停止自動採用'
|
||||
elif any(token in diagnostic_text for token in ('volume_conflict', 'weight_conflict', 'count_conflict')):
|
||||
label = '規格衝突待審'
|
||||
summary = f'{score_text},容量/件數不一致,已停止自動採用'
|
||||
elif 'type_conflict' in diagnostic_text:
|
||||
label = '品類衝突待審'
|
||||
summary = f'{score_text},品類不一致,已停止自動採用'
|
||||
else:
|
||||
label = '低信心待審'
|
||||
summary = f'{score_text},不自動採用以避免錯配'
|
||||
label, summary = _diagnostic_match_rejection_label(
|
||||
diagnostic_text,
|
||||
score_text,
|
||||
blocked=False,
|
||||
)
|
||||
return {
|
||||
'label': label,
|
||||
'tone': 'neutral',
|
||||
|
||||
@@ -63,3 +63,23 @@ def test_dashboard_match_status_distinguishes_expired_and_legacy_rows():
|
||||
assert expired["tone"] == "watch"
|
||||
assert legacy["label"] == "舊版配對待重驗"
|
||||
assert "identity_v2" in legacy["summary"]
|
||||
|
||||
|
||||
def test_dashboard_match_status_explains_identity_veto_reason():
|
||||
from routes.dashboard_routes import _build_pchome_match_status
|
||||
|
||||
bundle = _build_pchome_match_status({
|
||||
"attempt_status": "identity_veto",
|
||||
"best_match_score": 0.32,
|
||||
"error_message": "score=0.32; reasons=bundle_offer_conflict,product_line_conflict",
|
||||
})
|
||||
refill = _build_pchome_match_status({
|
||||
"attempt_status": "identity_veto",
|
||||
"best_match_score": 0.32,
|
||||
"error_message": "score=0.32; reasons=refill_pack_conflict",
|
||||
})
|
||||
|
||||
assert bundle["label"] == "組合差異待審"
|
||||
assert "組合包/多件組" in bundle["summary"]
|
||||
assert refill["label"] == "補充包差異待審"
|
||||
assert "補充瓶/補充包" in refill["summary"]
|
||||
|
||||
Reference in New Issue
Block a user