This commit is contained in:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user