V10.400 normalize cushion refill pack matches
All checks were successful
CD Pipeline / deploy (push) Successful in 1m4s

This commit is contained in:
OoO
2026-05-24 12:27:12 +08:00
committed by AiderHeal Bot
parent 8b4bcdf277
commit bbf21b81af
4 changed files with 191 additions and 4 deletions

View File

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

View File

@@ -13,6 +13,7 @@
## 📅 詳細更新日誌 (考古存檔)
### 2026-05-24PChome 近門檻身份回收第二輪
- **V10.400 氣墊粉餅補充蕊包數正規化**: marketplace matcher 針對氣墊粉餅新增保守的 `cushion_refill_pack_alignment`,只在一側明確為 `一盒兩蕊 / 2蕊`、另一側為單規格 `15g x2` 這類乘數包裝時,解除 `multi_component_conflict` 並進 `identity_review`。CLIO 羽緻無限緞光氣墊粉餅「一盒兩蕊」可被同款覆核;`2盒4蕊``15g x2` 仍維持 hard veto。同版補香氛蠟燭、TOKYO 車用消臭芳香劑、融蠟燈與有機護膚油 search identity anchors。離線 audit 759 筆 accepted 從 753 提升到 754剩餘 5 筆 fresh veto 皆為應擋的套組/品類差異。
- **V10.399 多香味 catalog 價格告警降級**: marketplace matcher 新增 `variant_selection_review`,當一側是無明確選項的商品線、另一側列出多個具名香味/款式選項時,允許同款身份回收但只進 `identity_review`,不直接進 `price_alert_exact`。首個正式案例是 HH 女性私密衣物抗菌手洗精 200ml 對 PChome 白麝香/清新花園/寶貝粉香多香味 listing此規則避免把多香味 catalog 價格誤當單一 variant 精準比價。
- **V10.398 true low confidence 保守回收**: marketplace matcher 針對正式前段 `true_low_confidence` 補一輪 focused exact identity lines讓 Baan 嬰兒修護唇膏、植村秀 3D 極細防水眼線膠筆、YSL 恆久完美透膚煙染腮紅、HH 私密植萃美白緊緻凝露、Lab52 學習刷牙漱口水、Benefit 經典菲菲染唇液、Herb24 晨霧純精油擴香儀、Pavaruni 40 香味 10ml 精油與 GATSBY 爆水擦澡濕巾等近門檻真同款可被回收;未放寬 `MIN_MATCH_SCORE`。同版保留 peripera 多色任選對單一色號、LUNASOL 頰彩對眼彩組、MUJI 細軸棉棒對黑色棉棒的低信心保護,並讓多組件套組即使達強身份證據也停在 `identity_review`,避免總價被誤當精準價格告警。

View File

@@ -362,6 +362,10 @@ SEARCH_IDENTITY_ANCHORS = (
"晨霧純精油擴香儀",
"天然植物香氛精油",
"爆水擦澡濕巾",
"香氛蠟燭20種香味",
"tokyo車用夾式消臭芳香劑",
"北歐簡樸融蠟燈桌面氣氛夜燈",
"大地有機植萃護膚油",
"3d立體持色眉彩盤",
"細芯睛彩雙頭眉筆",
"雙頭旋轉極細眉筆",
@@ -1315,6 +1319,51 @@ def _multi_component_count(identity: ProductIdentity) -> int:
return len(parts) if len(parts) > 1 else 1
def _repeated_single_spec_count(identity: ProductIdentity) -> Optional[int]:
text = _component_separator_text(identity)
matches = re.findall(
r"\d+(?:\.\d+)?\s*(?:ml|g|mg|毫升|公克|毫克)\s*x\s*(\d+)",
text,
flags=re.I,
)
if len(matches) != 1:
return None
try:
count = int(matches[0])
except (TypeError, ValueError):
return None
return count if count > 1 else None
def _refill_piece_count(identity: ProductIdentity) -> Optional[int]:
refill_counts = [
count
for count, unit in identity.counts
if _count_unit_family(unit) == "refill"
]
return max(refill_counts) if refill_counts else None
def _has_cushion_refill_pack_alignment(left: ProductIdentity, right: ProductIdentity) -> bool:
"""Align cushion compact refill language such as `一盒兩蕊` with `15g x2`."""
if left.product_type != "氣墊粉餅" or right.product_type != "氣墊粉餅":
return False
def aligned(refill_side: ProductIdentity, spec_side: ProductIdentity) -> bool:
refill_count = _refill_piece_count(refill_side)
spec_count = _repeated_single_spec_count(spec_side)
if not refill_count or not spec_count or refill_count != spec_count:
return False
box_counts = [
count
for count, unit in refill_side.counts
if unit in {"", "", ""} and count > 1
]
return not box_counts
return aligned(left, right) or aligned(right, left)
def _has_refill_pack(identity: ProductIdentity) -> bool:
text = identity.normalized_name
return bool(
@@ -1735,8 +1784,11 @@ def score_marketplace_match(
)
if bundle_offer_conflict:
reasons.append("bundle_offer_conflict")
if _has_multi_component(left) != _has_multi_component(right):
cushion_refill_pack_alignment = _has_cushion_refill_pack_alignment(left, right)
if _has_multi_component(left) != _has_multi_component(right) and not cushion_refill_pack_alignment:
reasons.append("multi_component_conflict")
if cushion_refill_pack_alignment:
reasons.append("cushion_refill_pack_alignment")
multi_component_count_conflict = (
_has_multi_component(left)
and _has_multi_component(right)
@@ -1770,7 +1822,7 @@ def score_marketplace_match(
hard_veto = brand_conflict or spec_conflict
if bundle_offer_conflict:
hard_veto = True
if _has_multi_component(left) != _has_multi_component(right):
if _has_multi_component(left) != _has_multi_component(right) and not cushion_refill_pack_alignment:
hard_veto = True
if multi_component_count_conflict:
hard_veto = True
@@ -1898,6 +1950,18 @@ def score_marketplace_match(
):
score += 0.025
reasons.append("strong_exact_spec_match")
if (
cushion_refill_pack_alignment
and brand_score >= 0.95
and not hard_veto
and price_penalty == 0
and type_score >= 0.95
and token_score >= 0.65
and sequence_score >= 0.65
and not variant_descriptor_conflict
):
score += 0.04
reasons.append("cushion_refill_pack_alignment_score")
if (
focused_exact_line_reason
and brand_score >= 0.95
@@ -2606,6 +2670,19 @@ def _has_pavaruni_40_scent_oil_alignment(left: ProductIdentity, right: ProductId
)
def _has_pavaruni_20_scent_candle_alignment(left: ProductIdentity, right: ProductIdentity) -> bool:
left_text = left.searchable_name
right_text = right.searchable_name
return (
"pavaruni" in (left.brand_tokens & right.brand_tokens)
and "香氛蠟燭" in left_text
and "香氛蠟燭" in right_text
and _has_shared_weight(left, right, 450)
and ("20香味" in left_text or "20種香味" in left_text)
and ("20香味" in right_text or "20種香味" in right_text)
)
def _has_shared_count(left: ProductIdentity, right: ProductIdentity, count: int, unit: str) -> bool:
return (count, unit) in set(left.counts) and (count, unit) in set(right.counts)
@@ -2616,6 +2693,12 @@ def _has_shared_volume(left: ProductIdentity, right: ProductIdentity, volume_ml:
)
def _has_shared_weight(left: ProductIdentity, right: ProductIdentity, weight_g: float) -> bool:
return any(_close_number(value, weight_g) for value in left.weights_g) and any(
_close_number(value, weight_g) for value in right.weights_g
)
def _has_focused_low_score_exact_identity_line(left: ProductIdentity, right: ProductIdentity) -> str:
left_text = left.searchable_name
right_text = right.searchable_name
@@ -2761,6 +2844,42 @@ def _has_focused_low_score_exact_identity_line(left: ProductIdentity, right: Pro
return "herb24_mist_diffuser_black"
if _has_pavaruni_40_scent_oil_alignment(left, right):
return "pavaruni_40_scent_oil"
if _has_pavaruni_20_scent_candle_alignment(left, right):
return "pavaruni_20_scent_candle"
if (
{"laundrin", "朗德林"} & (left.brand_tokens & right.brand_tokens)
and "tokyo" in left_text
and "tokyo" in right_text
and "車用" in left_text
and "車用" in right_text
and "芳香劑" in left_text
and "芳香劑" in right_text
and _has_shared_count(left, right, 1, "")
):
return "laundrin_tokyo_car_freshener"
if (
"好物良品" in (left.brand_tokens & right.brand_tokens)
and "北歐簡樸融蠟燈桌面氣氛夜燈" in left_text
and "北歐簡樸融蠟燈桌面氣氛夜燈" in right_text
):
return "goodgoods_nordic_wax_lamp"
if (
{"derma", "丹麥德瑪"} & (left.brand_tokens & right.brand_tokens)
and "有機植萃" in left_text
and "有機植萃" in right_text
and "護膚油" in left_text
and "護膚油" in right_text
and _has_shared_volume(left, right, 150)
):
return "derma_eco_skin_oil"
if (
{"yuskin", "悠斯晶"} & (left.brand_tokens & right.brand_tokens)
and "乳霜" in left_text
and "乳霜" in right_text
and _has_shared_weight(left, right, 30)
and _has_shared_count(left, right, 6, "")
):
return "yuskin_classic_cream_30g_6pack"
if (
"gatsby" in (left.brand_tokens & right.brand_tokens)
and "爆水擦澡濕巾" in left_text
@@ -2829,6 +2948,8 @@ def _has_variant_descriptor_conflict(left: ProductIdentity, right: ProductIdenti
return False
if _has_pavaruni_40_scent_oil_alignment(left, right):
return False
if _has_pavaruni_20_scent_candle_alignment(left, right):
return False
if _is_relove_private_cleanser_line(left, right):
return False
if (

View File

@@ -859,6 +859,38 @@ def test_marketplace_matcher_keeps_named_scent_catalog_in_identity_review():
assert "variant_selection_review" in diagnostics.reasons
def test_marketplace_matcher_aligns_cushion_one_box_two_refills_with_spec_times_two():
from services.marketplace_product_matcher import score_marketplace_match
diagnostics = score_marketplace_match(
"【CLIO 珂莉奧 官方直營】羽緻無限緞光氣墊粉餅 SPF50+ PA+++(任選 一盒兩蕊)",
"珂莉奧 羽緻無限緞光氣墊粉餅 SPF50+, PA+++15g*2",
momo_price=885,
competitor_price=808,
)
assert diagnostics.score >= 0.76
assert diagnostics.hard_veto is False
assert diagnostics.alert_tier == "identity_review"
assert "cushion_refill_pack_alignment" in diagnostics.reasons
assert "multi_component_conflict" not in diagnostics.reasons
def test_marketplace_matcher_keeps_cushion_four_refills_vs_spec_times_two_vetoed():
from services.marketplace_product_matcher import score_marketplace_match
diagnostics = score_marketplace_match(
"即期品【CLIO 珂莉奧 官方直營】玫瑰精萃亮采氣墊粉餅SPF 50+ PA++++(2入組-任選 共2盒4蕊)",
"CLIO珂莉奧 粉鑽亮采氣墊粉餅 SPF50 PA (15gX2)",
momo_price=999,
competitor_price=1150,
)
assert diagnostics.score < 0.76
assert diagnostics.hard_veto is True
assert "multi_component_conflict" in diagnostics.reasons
def test_marketplace_matcher_promotes_multi_variant_catalog_listings():
from services.marketplace_product_matcher import score_marketplace_match
@@ -1069,6 +1101,31 @@ def test_marketplace_matcher_promotes_focused_low_score_exact_identity_lines():
"【美國Pavaruni】天然植物香氛精油40種香味10ml 多款任選",
"focused_exact_identity_pavaruni_40_scent_oil",
),
(
"【Pavaruni】美國香氛蠟燭20種香味450g(禮盒大豆蠟植物天然精油花香木質果香生日聖誕交換女友女生情人禮物)",
"【美國Pavaruni】香氛蠟燭20種香味450g 卡啡那系列 多款任選",
"focused_exact_identity_pavaruni_20_scent_candle",
),
(
"【日本Laundrin朗德林】TOKYO汽車空調出風口專用夾式消臭芳香劑1入/袋(車用清新擴香劑香氛淨化馨香約30天)",
"日本Laundrin朗德林-TOKYO車用夾式消臭芳香劑1入/袋",
"focused_exact_identity_laundrin_tokyo_car_freshener",
),
(
"【好物良品】北歐簡樸融蠟燈桌面氣氛夜燈|附燈泡(氛圍燈 室內芳香 交換禮物 桌上檯燈 香氛蠟燭 擴香)",
"【好物良品】北歐簡樸融蠟燈桌面氣氛夜燈(2款任選附燈泡)",
"focused_exact_identity_goodgoods_nordic_wax_lamp",
),
(
"【Derma 丹麥德瑪】大地有機植萃撫紋護膚油 150ml(有機植物油複合配方 滋潤不黏膩)",
"Derma 大地 Eco 有機植萃護膚油 150ml",
"focused_exact_identity_derma_eco_skin_oil",
),
(
"【悠斯晶】日本No.1維他命乳霜★經典乳霜30g(攜帶型 6入組)",
"【Yuskin悠斯晶】A乳霜 攜帶型 6盒組(30g/盒)",
"focused_exact_identity_yuskin_classic_cream_30g_6pack",
),
(
"【GATSBY】爆水擦澡濕巾24張入(涼感乾洗澡)",
"GATSBY 爆水擦澡濕巾24張入(240g)",
@@ -1102,8 +1159,16 @@ def test_marketplace_matcher_keeps_high_variant_low_score_lines_outside_focused_
"【MUJI 無印良品】細軸棉棒/200支",
"【MUJI 無印良品】棉棒(黑色)/200支",
)
kate_limited = score_marketplace_match(
"【KATE 凱婷】絕美綻放睫毛膏(NANA限量聯名款)",
"【KATE 凱婷】絕美綻放睫毛膏 7.1g",
)
schick_bundle_gap = score_marketplace_match(
"【Schick 舒適牌】舒芙仕女除毛刀把刀片組 (1刀把2刀片+刀片3入)",
"【Schick舒適牌】舒綺 仕女除毛刀 敏感肌用 1刀把+2刀片 送卡娜赫拉束口袋",
)
for diagnostics in (lush, lactacyd, lunasol, muji_swab):
for diagnostics in (lush, lactacyd, lunasol, muji_swab, kate_limited, schick_bundle_gap):
assert diagnostics.score < 0.76
assert not any(reason.startswith("focused_exact_identity_") for reason in diagnostics.reasons)