V10.517 強化近門檻商品比對防線
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
================================================================================
|
||||
|
||||
【已完成】
|
||||
- V10.517 補 PChome 近門檻比對安全 exact 與香氛 variant 防線:Lab52 齒妍堂汪汪隊嬰幼兒牙刷 2 入組可由低分區提升為 `exact / total_price / price_alert_exact`;Les nez 香氛融蠟燈不同款式、Time Leisure 香薰蠟燭單側香味款式會被留在覆核 / veto,不再進 recoverable 自動回刷,避免為了壓低 low_score 而錯配款式。
|
||||
- V10.515 補 Webcrumbs host data 硬性授權:即使正式環境 `DISABLE_LOGIN=true` 讓一般 `@login_required` 放行,`/api/webcrumbs/marketplace-host-data` 仍必須有登入 session 或 `X-Internal-Key` 才能取真實 SKU/價差;`/webcrumbs` 未授權時只注入 `auth_required` 空狀態,避免 inline seed data 公開正式比價資料。
|
||||
- V10.514 新增 Webcrumbs MOMO/PChome host data read-only API:`/api/webcrumbs/marketplace-host-data` 回傳與 `/webcrumbs` inline seed 相同的登入後 JSON contract,提供 plugin / QA / 其他專案 proxy 驗證;API boundary 明確標示不寫 DB、不呼叫 LLM、不抓外站,只允許 exact / total_price / price_alert_exact 價差摘要。
|
||||
- V10.513 外部工具診斷頁 payload 模組化:新增 `services/external_tool_payload_service.py`,把 Metabase/Grist/Webcrumbs 的診斷 payload 與 Webcrumbs host data 組裝移出 `routes/system_public_routes.py`,讓 route 回到 HTTP glue,`system_public_routes.py` 從 600+ 行降至 500 行內。
|
||||
|
||||
@@ -402,7 +402,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
|
||||
# ==========================================
|
||||
# 系統版本與路徑
|
||||
# ==========================================
|
||||
SYSTEM_VERSION = "V10.516"
|
||||
SYSTEM_VERSION = "V10.517"
|
||||
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
|
||||
public_url = PUBLIC_URL # 用於模板顯示
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
## 📅 詳細更新日誌 (考古存檔)
|
||||
|
||||
### 2026-05-31:Webcrumbs 共用 UI Runtime 與市場情報 writer approval
|
||||
- **V10.517 PChome near-threshold 比對 hotfix**: 新增 Lab52 齒妍堂汪汪隊嬰幼兒牙刷 2 入組 focused exact identity,讓真同款可進 `exact / total_price / price_alert_exact`;同時補 Les nez 香氛融蠟燈款式選擇 gap 與 Time Leisure 香薰蠟燭香味 gap,將不同款式 / 單側香味候選留在覆核或 veto,不讓它們進 recoverable 自動回刷。測試鎖住 Dashing Diva、Pavaruni、Recipe Box、Lactacyd 與 feeder recoverable 邊界。
|
||||
- **V10.516 Webcrumbs host data 授權回歸測試**: 新增 Flask runtime 測試,直接重現 `DISABLE_LOGIN=true` 下 `/api/webcrumbs/marketplace-host-data` 必須回 401 且不得組裝真實 host data;同時鎖住 `X-Internal-Key` 與登入 session 可取得敏感 seed、未授權 `/webcrumbs` 只注入 `auth_required` 空狀態,避免後續改動再讓 public 診斷頁洩漏 MOMO/PChome SKU 與價差。
|
||||
- **V10.515 Webcrumbs host data 硬性授權**: 發現正式環境一般 `@login_required` 可能因 `DISABLE_LOGIN=true` 放行後,為 `/api/webcrumbs/marketplace-host-data` 與 `/webcrumbs` inline seed 加上獨立授權判斷;只有登入 session 或 `X-Internal-Key` 可取得真實 SKU/價差,未授權時只回 `auth_required` 空狀態,避免 public runtime 診斷頁洩漏正式比價資料。
|
||||
- **V10.514 Webcrumbs host data read-only API**: 新增登入後 `/api/webcrumbs/marketplace-host-data`,回傳與 `/webcrumbs` inline seed 相同的 MOMO/PChome exact 價差 host data contract,供 plugin、QA 與其他專案 proxy 驗證;API boundary 明確標示 `writes_database=false`、`calls_llm=false`、`fetches_external=false`,且只允許 exact / total_price / price_alert_exact 摘要。
|
||||
|
||||
@@ -495,6 +495,7 @@ FOCUSED_IDENTITY_VARIANT_REVIEW_BYPASS_REASONS = {
|
||||
"cetaphil_long_lotion_237ml",
|
||||
"cetaphil_long_lotion_473ml",
|
||||
"clarins_double_serum_eye_20ml",
|
||||
"lab52_paw_patrol_baby_toothbrush_2pack",
|
||||
"derma_baby_wash_150ml",
|
||||
"derma_baby_wash_500ml",
|
||||
"physiogel_ai_ice_essence_200ml_2pack",
|
||||
@@ -528,6 +529,7 @@ FOCUSED_IDENTITY_TOTAL_PRICE_REASONS = {
|
||||
"cetaphil_long_lotion_237ml",
|
||||
"cetaphil_long_lotion_473ml",
|
||||
"clarins_double_serum_eye_20ml",
|
||||
"lab52_paw_patrol_baby_toothbrush_2pack",
|
||||
"derma_baby_wash_150ml",
|
||||
"derma_baby_wash_500ml",
|
||||
"physiogel_ai_ice_essence_200ml_2pack",
|
||||
@@ -606,6 +608,9 @@ VARIANT_OPTION_COLOR_WORDS = {
|
||||
"海鹽",
|
||||
"檸檬草",
|
||||
"茶樹",
|
||||
"英國梨",
|
||||
"小蒼蘭",
|
||||
"英國梨小蒼蘭",
|
||||
"櫻花",
|
||||
"繡球花",
|
||||
"魔髮奇緣",
|
||||
@@ -2104,6 +2109,9 @@ def score_marketplace_match(
|
||||
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")
|
||||
aroma_lamp_style_selection_gap = _has_aroma_lamp_style_selection_gap(left, right)
|
||||
if aroma_lamp_style_selection_gap:
|
||||
reasons.append("aroma_lamp_style_selection_gap")
|
||||
hooome_wax_lamp_design_gap = _has_hooome_wax_lamp_design_gap(left, right)
|
||||
if hooome_wax_lamp_design_gap:
|
||||
reasons.append("hooome_wax_lamp_design_gap")
|
||||
@@ -2146,6 +2154,7 @@ def score_marketplace_match(
|
||||
or relove_private_cleanser_variant_gap
|
||||
or candle_catalog_selection_gap
|
||||
or bath_additive_variant_gap
|
||||
or aroma_lamp_style_selection_gap
|
||||
or hooome_wax_lamp_design_gap
|
||||
or makeup_catalog_selection_gap
|
||||
or loreal_serum_variant_gap
|
||||
@@ -3325,6 +3334,49 @@ def _has_aroma_scent_variant_conflict(left: ProductIdentity, right: ProductIdent
|
||||
return False
|
||||
|
||||
|
||||
def _has_aroma_lamp_style_selection_gap(left: ProductIdentity, right: ProductIdentity) -> bool:
|
||||
pair_text = f"{left.searchable_name} {right.searchable_name}"
|
||||
if not ({"les", "nez", "香鼻子"} & (left.brand_tokens & right.brand_tokens)):
|
||||
return False
|
||||
if not any(term in pair_text for term in ("融蠟燈", "融燭燈", "蠟燭暖燈", "香氛燈")):
|
||||
return False
|
||||
if not any(term in left.searchable_name for term in ("融蠟燈", "融燭燈", "蠟燭暖燈", "香氛燈")):
|
||||
return False
|
||||
if not any(term in right.searchable_name for term in ("融蠟燈", "融燭燈", "蠟燭暖燈", "香氛燈")):
|
||||
return False
|
||||
|
||||
style_aliases = {
|
||||
"流金歲月": ("流金歲月",),
|
||||
"暮光琥珀": ("暮光琥珀",),
|
||||
"閃耀琥珀": ("閃耀琥珀",),
|
||||
"星夜": ("星夜款", "星夜"),
|
||||
"流光玫瑰金": ("流光玫瑰金", "玫瑰金"),
|
||||
"土耳其風": ("土耳其風",),
|
||||
"手工拼貼玻璃": ("手工拼貼玻璃",),
|
||||
"手工玻璃": ("手工玻璃",),
|
||||
"北歐": ("北歐",),
|
||||
"水晶燈": ("水晶燈",),
|
||||
}
|
||||
left_styles = {
|
||||
style
|
||||
for style, aliases in style_aliases.items()
|
||||
if any(alias in left.searchable_name for alias in aliases)
|
||||
}
|
||||
right_styles = {
|
||||
style
|
||||
for style, aliases in style_aliases.items()
|
||||
if any(alias in right.searchable_name for alias in aliases)
|
||||
}
|
||||
if not left_styles and not right_styles:
|
||||
return False
|
||||
if left_styles == right_styles:
|
||||
return False
|
||||
shared_styles = left_styles & right_styles
|
||||
left_specific = left_styles - shared_styles
|
||||
right_specific = right_styles - shared_styles
|
||||
return bool(left_specific or right_specific)
|
||||
|
||||
|
||||
def _has_core_ingredient_line_conflict(left: ProductIdentity, right: ProductIdentity) -> bool:
|
||||
pair_text = f"{left.searchable_name} {right.searchable_name}"
|
||||
if not any(term in pair_text for term in ("油膏", "護膚油", "身體油", "精油", "基礎油", "按摩油", "甜杏仁油", "酪梨油", "霜", "乳霜")):
|
||||
@@ -4113,6 +4165,15 @@ def _has_focused_low_score_exact_identity_line(left: ProductIdentity, right: Pro
|
||||
and _has_overlapping_base_spec(left, right)
|
||||
):
|
||||
return "lab52_mouthwash"
|
||||
if (
|
||||
{"lab52", "齒妍堂"} & (left.brand_tokens | right.brand_tokens)
|
||||
and "牙刷" in left_text
|
||||
and "牙刷" in right_text
|
||||
and any(term in left_text for term in ("嬰幼兒", "幼兒", "汪汪隊"))
|
||||
and any(term in right_text for term in ("嬰幼兒", "幼兒", "汪汪隊"))
|
||||
and _has_shared_count(left, right, 2, "入")
|
||||
):
|
||||
return "lab52_paw_patrol_baby_toothbrush_2pack"
|
||||
if (
|
||||
"benefit" in (left.brand_tokens & right.brand_tokens)
|
||||
and "染唇液" in left_text
|
||||
|
||||
@@ -576,6 +576,11 @@ def test_marketplace_matcher_promotes_focused_exact_pack_rows_to_total_price():
|
||||
"台隆手創館 CANMAKE淚袋專用盤(淚袋眼影盤)",
|
||||
"focused_exact_identity_canmake_tear_bag_palette",
|
||||
),
|
||||
(
|
||||
"【Lab52 齒妍堂】小頭軟毛嬰幼兒牙刷2入/組(幼兒專用/牙齒清潔/柔軟刷毛/汪汪隊牙刷/呵護幼兒牙齦)",
|
||||
"Lab52齒妍堂 汪汪隊立大功小頭軟毛嬰幼兒牙刷(2入/組)",
|
||||
"focused_exact_identity_lab52_paw_patrol_baby_toothbrush_2pack",
|
||||
),
|
||||
]
|
||||
|
||||
for momo_name, competitor_name, expected_reason in cases:
|
||||
@@ -2437,6 +2442,25 @@ def test_marketplace_matcher_promotes_safe_aroma_lamps_to_total_price():
|
||||
assert "focused_exact_identity_hooome_classic_white_wax_lamp_bulbs_giftbox" in hooome.reasons
|
||||
|
||||
|
||||
def test_marketplace_matcher_blocks_aroma_lamp_style_selection_from_recoverable():
|
||||
from services.marketplace_product_matcher import score_marketplace_match
|
||||
|
||||
named_style_gap = score_marketplace_match(
|
||||
"【Les nez 香鼻子】香氛融蠟燈 流金歲月(香氛/融蠟燈/交換禮物)",
|
||||
"【Les nez 香鼻子】香氛融蠟燈 暮光琥珀(手工玻璃)",
|
||||
)
|
||||
single_sided_style_gap = score_marketplace_match(
|
||||
"【Les nez 香鼻子】北歐香氛融蠟燈(香氛/融蠟燈)",
|
||||
"【Les nez 香鼻子】北歐香氛水晶燈融蠟燈 流光玫瑰金",
|
||||
)
|
||||
|
||||
for diagnostics in (named_style_gap, single_sided_style_gap):
|
||||
assert diagnostics.hard_veto is False
|
||||
assert diagnostics.price_basis == "manual_review" or diagnostics.score < 0.76
|
||||
assert "aroma_lamp_style_selection_gap" in diagnostics.reasons
|
||||
assert "variant_selection_review" in diagnostics.reasons
|
||||
|
||||
|
||||
def test_marketplace_matcher_keeps_recipe_box_child_polish_catalog_in_review():
|
||||
from services.marketplace_product_matcher import score_marketplace_match
|
||||
|
||||
@@ -2625,6 +2649,10 @@ def test_marketplace_matcher_blocks_scent_and_core_line_conflicts():
|
||||
"【AUS LIFE 澳思萊】檸檬草滾珠精油 5.3ml",
|
||||
"【AUS LIFE澳思萊】茶樹滾珠精油5.3ml",
|
||||
)
|
||||
candle_scent_gap = score_marketplace_match(
|
||||
"【Time Leisure】聖誕節交換禮物香薰蠟燭擺飾禮盒",
|
||||
"Time Leisure 聖誕節交換禮物香薰蠟燭擺飾禮盒 英國梨小蒼蘭",
|
||||
)
|
||||
ingredient_gap = score_marketplace_match(
|
||||
"【NOW娜奧】純椰子油膏207ml-Now Foods",
|
||||
"【NOW 娜奧】Now Foods 純乳木果油油膏 207ml",
|
||||
@@ -2636,6 +2664,8 @@ def test_marketplace_matcher_blocks_scent_and_core_line_conflicts():
|
||||
|
||||
assert rolling_oil_scent_gap.hard_veto is True
|
||||
assert "aroma_scent_variant_conflict" in rolling_oil_scent_gap.reasons
|
||||
assert candle_scent_gap.hard_veto is True
|
||||
assert "aroma_scent_variant_conflict" in candle_scent_gap.reasons
|
||||
assert ingredient_gap.hard_veto is True
|
||||
assert "core_ingredient_line_conflict" in ingredient_gap.reasons
|
||||
assert powder_line_gap.hard_veto is True
|
||||
|
||||
Reference in New Issue
Block a user