讓 PChome feeder 採納人工覆核回饋
All checks were successful
CD Pipeline / deploy (push) Successful in 1m8s

This commit is contained in:
OoO
2026-05-20 10:06:46 +08:00
parent 756b01af66
commit 45e1aad308
5 changed files with 159 additions and 5 deletions

View File

@@ -716,6 +716,37 @@ class CompetitorPriceFeeder:
f"incoming_score={match_score:.3f}",
)
def _fetch_latest_manual_review_for_candidate(
self,
sku: str,
competitor_product_id: str,
source: str = "pchome",
) -> Optional[dict]:
"""Read the latest human review for this exact candidate, if the table exists."""
if not competitor_product_id:
return None
from sqlalchemy import text
try:
with self.engine.connect() as conn:
row = conn.execute(text("""
SELECT review_action, review_reason, reviewer_identity, reviewed_at
FROM competitor_match_reviews
WHERE sku = :sku
AND source = :source
AND candidate_product_id = :candidate_id
ORDER BY reviewed_at DESC, id DESC
LIMIT 1
"""), {
"sku": sku,
"source": source,
"candidate_id": competitor_product_id,
}).mappings().first()
except Exception:
return None
return dict(row) if row else None
def _run_sku_items(self, skus: list, source: str = "pchome", label: str = "PChome 競品價格") -> FeederResult:
start = time.time()
@@ -777,7 +808,57 @@ class CompetitorPriceFeeder:
continue
best_product, score, diagnostics = result
if getattr(diagnostics, "comparison_mode", "") == "unit_comparable":
manual_review = self._fetch_latest_manual_review_for_candidate(
sku,
getattr(best_product, "product_id", None),
source=source,
)
manual_action = (manual_review or {}).get("review_action")
if manual_action == "reject_identity":
logger.info(
f"[Feeder] {sku} 候選已被人工否決,跳過正式寫入 | "
f"candidate={getattr(best_product, 'product_id', None)}"
)
self._record_match_attempt(
sku,
momo_name,
momo_product_id=momo_product_id,
momo_price=momo_price,
search_terms=search_terms,
candidate_count=len(products),
attempt_status="manual_rejected",
best_product=best_product,
best_score=score,
error_message=f"manual_review_rejected; {_format_match_diagnostics(diagnostics)}",
source=source,
)
attempts_written += 1
skipped_low += 1
continue
if manual_action == "unit_price_required":
logger.info(
f"[Feeder] {sku} 候選已被人工標記為單位價比較,不寫正式總價差 | "
f"candidate={getattr(best_product, 'product_id', None)}"
)
self._record_match_attempt(
sku,
momo_name,
momo_product_id=momo_product_id,
momo_price=momo_price,
search_terms=search_terms,
candidate_count=len(products),
attempt_status="manual_unit_price_required",
best_product=best_product,
best_score=score,
error_message=f"manual_review_unit_price_required; {_format_match_diagnostics(diagnostics)}",
source=source,
)
attempts_written += 1
skipped_low += 1
continue
manual_accept_override = manual_action == "accept_identity"
if getattr(diagnostics, "comparison_mode", "") == "unit_comparable" and not manual_accept_override:
logger.info(
f"[Feeder] {sku} 候選屬單位價可比但非同販售組合,"
f"不寫入正式價差 | {_format_match_diagnostics(diagnostics)}"
@@ -799,7 +880,7 @@ class CompetitorPriceFeeder:
skipped_low += 1
continue
if score < MIN_MATCH_SCORE:
if score < MIN_MATCH_SCORE and not manual_accept_override:
logger.debug(
f"[Feeder] {sku} 比對分數過低 ({score:.3f} < {MIN_MATCH_SCORE})"
f"{_format_match_diagnostics(diagnostics)}"
@@ -821,10 +902,15 @@ class CompetitorPriceFeeder:
skipped_low += 1
continue
if manual_accept_override:
score = max(score, MIN_MATCH_SCORE)
tags = _extract_tags(best_product)
tags.extend(getattr(diagnostics, "tags", []))
for reason in getattr(diagnostics, "reasons", ()) or ():
tags.append(f"match_{reason}")
if manual_accept_override:
tags.extend(["manual_review", "manual_accept"])
tags = [tag for tag in tags if tag != "identity_veto"]
tags = list(dict.fromkeys(tags))
should_write, write_reason = self._should_upsert_competitor_price(
sku,
@@ -832,6 +918,9 @@ class CompetitorPriceFeeder:
score,
source=source,
)
if manual_accept_override and not should_write:
should_write = True
write_reason = "manual_accept_override"
if not should_write:
logger.info(f"[Feeder] {sku} 進入人工覆核,不覆蓋既有配對 | {write_reason}")
self._record_match_attempt(