強化 PChome accepted queue 變體防線
All checks were successful
CD Pipeline / deploy (push) Successful in 1m4s

This commit is contained in:
OoO
2026-05-25 21:35:41 +08:00
parent 50daad84a7
commit e6b48cefa8
7 changed files with 170 additions and 12 deletions

View File

@@ -350,11 +350,6 @@ def fetch_variant_rescore_accept_review_rows(
limit: int = 100,
) -> list[dict[str, Any]]:
"""Fetch latest rescore-accepted rows that should be retracted to review."""
reason_predicate = (
"diagnostic_codes::text LIKE :reason_filter"
if conn.dialect.name == "postgresql"
else "CAST(diagnostic_codes AS TEXT) LIKE :reason_filter"
)
nulls_last = " NULLS LAST" if conn.dialect.name == "postgresql" else ""
sql = text(f"""
WITH ranked AS (
@@ -400,19 +395,30 @@ def fetch_variant_rescore_accept_review_rows(
FROM ranked
WHERE rn = 1
AND attempt_status = :attempt_status
AND {reason_predicate}
ORDER BY attempted_at DESC{nulls_last}
LIMIT :limit
""")
return [
rows = [
dict(row)
for row in conn.execute(sql, {
"source": source,
"attempt_status": RESCORE_ACCEPTED_CURRENT_STATUS,
"reason_filter": "%variant_selection_review%",
"limit": max(1, int(limit)),
}).mappings().all()
]
retractable_rows: list[dict[str, Any]] = []
for row in rows:
diagnostic_text = json.dumps(row.get("diagnostic_codes"), ensure_ascii=False, default=str)
if "variant_selection_review" in diagnostic_text:
retractable_rows.append(row)
continue
decision = classify_match_attempt_row(row)
if (
"variant_selection_review" in set(decision.reasons)
and decision.suggested_status == "low_score_current"
):
retractable_rows.append(row)
return retractable_rows
def retract_variant_rescore_accept_reviews(

View File

@@ -1978,6 +1978,9 @@ def score_marketplace_match(
makeup_spray_line_conflict = _has_makeup_spray_line_conflict(left, right)
if makeup_spray_line_conflict:
reasons.append("makeup_spray_line_conflict")
romand_lip_line_conflict = _has_romand_lip_line_conflict(left, right)
if romand_lip_line_conflict:
reasons.append("romand_lip_line_conflict")
nail_tool_function_conflict = _has_nail_tool_function_conflict(left, right)
if nail_tool_function_conflict:
reasons.append("nail_tool_function_conflict")
@@ -2011,6 +2014,9 @@ def score_marketplace_match(
cleanser_lotion_line_conflict = _has_cleanser_lotion_line_conflict(left, right)
if cleanser_lotion_line_conflict:
reasons.append("cleanser_lotion_line_conflict")
selection1990_wax_lamp_design_conflict = _has_selection1990_wax_lamp_design_conflict(left, right)
if selection1990_wax_lamp_design_conflict:
reasons.append("selection1990_wax_lamp_design_conflict")
wax_lamp_size_letter_conflict = _has_wax_lamp_size_letter_conflict(left, right)
if wax_lamp_size_letter_conflict:
reasons.append("size_letter_variant_conflict")
@@ -2020,9 +2026,13 @@ def score_marketplace_match(
commercial_condition_gap = _has_commercial_condition_gap(left, right)
if commercial_condition_gap:
reasons.append("commercial_condition_gap")
relove_private_cleanser_variant_gap = _has_relove_private_cleanser_variant_gap(left, right)
if relove_private_cleanser_variant_gap:
reasons.append("relove_private_cleanser_variant_gap")
variant_selection_review = (
_has_named_variant_selection_review(left, right, shared_anchor)
or commercial_condition_gap
or relove_private_cleanser_variant_gap
)
if variant_selection_review:
reasons.append("variant_selection_review")
@@ -2062,6 +2072,8 @@ def score_marketplace_match(
hard_veto = True
if makeup_spray_line_conflict:
hard_veto = True
if romand_lip_line_conflict:
hard_veto = True
if nail_tool_function_conflict:
hard_veto = True
if schick_razor_line_conflict:
@@ -2084,6 +2096,8 @@ def score_marketplace_match(
hard_veto = True
if cleanser_lotion_line_conflict:
hard_veto = True
if selection1990_wax_lamp_design_conflict:
hard_veto = True
if wax_lamp_size_letter_conflict:
hard_veto = True
if nitori_diffuser_model_conflict:
@@ -2973,6 +2987,35 @@ def _has_makeup_spray_variant_selection_gap(left: ProductIdentity, right: Produc
return left_groups != right_groups
def _romand_lip_line_groups(identity: ProductIdentity) -> set[str]:
text = identity.searchable_name
groups: set[str] = set()
if "果汁唇釉" in text or "juicy" in text:
groups.add("juicy")
if "零絲絨" in text or "zero velvet" in text or "霧面唇釉" in text:
groups.add("zero_velvet")
if "果凍唇釉" in text or "glasting" in text or "唇凍" in text:
groups.add("glasting")
if "水感唇釉" in text:
groups.add("water_gloss")
return groups
def _has_romand_lip_line_conflict(left: ProductIdentity, right: ProductIdentity) -> bool:
pair_text = f"{left.searchable_name} {right.searchable_name}"
if not (
{"rom", "romand"} & (left.brand_tokens | right.brand_tokens)
or "rom&nd" in pair_text
or "romand" in pair_text
):
return False
if "" not in left.searchable_name or "" not in right.searchable_name:
return False
left_groups = _romand_lip_line_groups(left)
right_groups = _romand_lip_line_groups(right)
return bool(left_groups and right_groups and left_groups.isdisjoint(right_groups))
def _has_nail_tool_function_conflict(left: ProductIdentity, right: ProductIdentity) -> bool:
left_text = left.searchable_name
right_text = right.searchable_name
@@ -3202,6 +3245,31 @@ def _has_cleanser_lotion_line_conflict(left: ProductIdentity, right: ProductIden
return bool((left_cleanser and right_lotion) or (right_cleanser and left_lotion))
def _selection1990_wax_lamp_design_groups(identity: ProductIdentity) -> set[str]:
text = identity.searchable_name
groups: set[str] = set()
if "現代簡約半圓罩融燭燈" in text or "半圓罩" in text:
groups.add("half_dome")
if "歐式可彎融燭燈" in text or "可彎融燭燈" in text:
groups.add("bendable")
if "韓風原木底座融燭燈" in text or "原木底座融燭燈" in text:
groups.add("wood_base")
if "北歐簡樸融蠟燈" in text or "北歐簡樸" in text:
groups.add("nordic")
return groups
def _has_selection1990_wax_lamp_design_conflict(left: ProductIdentity, right: ProductIdentity) -> bool:
if not ({"1990", "選物"} <= (left.brand_tokens & right.brand_tokens)):
return False
pair_text = f"{left.searchable_name} {right.searchable_name}"
if not any(term in pair_text for term in ("融燭燈", "蠟燭暖燈", "融蠟燈")):
return False
left_groups = _selection1990_wax_lamp_design_groups(left)
right_groups = _selection1990_wax_lamp_design_groups(right)
return bool(left_groups and right_groups and left_groups.isdisjoint(right_groups))
def _standalone_size_letter_tokens(identity: ProductIdentity) -> set[str]:
text = identity.searchable_name
return {
@@ -3282,6 +3350,15 @@ def _has_commercial_condition_gap(left: ProductIdentity, right: ProductIdentity)
return bool(left_terms or right_terms) and left_terms != right_terms
def _has_relove_private_cleanser_variant_gap(left: ProductIdentity, right: ProductIdentity) -> bool:
if not _is_relove_cleanser_gel_like(left, right):
return False
brightening_terms = ("傳明酸", "淨白", "美白", "亮白", "菸鹼醯胺", "niacinamide")
left_brightening = any(term in left.searchable_name for term in brightening_terms)
right_brightening = any(term in right.searchable_name for term in brightening_terms)
return left_brightening != right_brightening
def _has_catalog_specific_variant_selection_gap(left: ProductIdentity, right: ProductIdentity) -> bool:
pair_text = f"{left.searchable_name} {right.searchable_name}"
if not any(
@@ -3888,6 +3965,18 @@ def _is_relove_private_cleanser_line(left: ProductIdentity, right: ProductIdenti
)
def _is_relove_cleanser_gel_like(left: ProductIdentity, right: ProductIdentity) -> bool:
if "relove" not in (left.brand_tokens | right.brand_tokens):
return False
cleanser_terms = ("私密", "潔淨", "清潔")
return (
"凝露" in left.searchable_name
and "凝露" in right.searchable_name
and any(term in left.searchable_name for term in cleanser_terms)
and any(term in right.searchable_name for term in cleanser_terms)
)
def _is_multi_variant_catalog_listing(identity: ProductIdentity) -> bool:
text = identity.normalized_name
return any(phrase in text for phrase in MULTI_VARIANT_LISTING_PHRASES)