fix: pick best targeted momo offer per pchome item
All checks were successful
CD Pipeline / deploy (push) Successful in 1m4s

This commit is contained in:
OoO
2026-06-19 00:55:35 +08:00
parent 9d84cbfd43
commit bed4488a72
3 changed files with 103 additions and 3 deletions

View File

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

View File

@@ -712,6 +712,31 @@ def _targeted_candidate_needs_review(candidate: dict[str, Any]) -> bool:
return bool(reasons & review_reason_markers)
def _targeted_candidate_sync_rank(candidate: dict[str, Any]) -> tuple[float, float, float, float]:
"""同一個 PChome 商品有多個候選時,挑最適合營運判斷的一筆。"""
auto_type = _targeted_candidate_auto_type(candidate)
type_rank = 3.0 if auto_type == "total_price" else 2.0 if auto_type == "unit_price" else 0.0
try:
match_score = float(candidate.get("target_match_score") or 0.0)
except (TypeError, ValueError):
match_score = 0.0
unit_price_comparison = (
candidate.get("target_unit_price_comparison")
if isinstance(candidate.get("target_unit_price_comparison"), dict)
else {}
)
momo_total = _to_float(unit_price_comparison.get("momo_total_quantity"))
pchome_total = _to_float(unit_price_comparison.get("competitor_total_quantity"))
quantity_delta = 999999.0
same_quantity = 0.0
if momo_total > 0 and pchome_total > 0:
quantity_delta = abs(momo_total - pchome_total)
same_quantity = 1.0 if quantity_delta <= 0.0001 else 0.0
return (type_rank, same_quantity, -quantity_delta, match_score)
def _targeted_candidate_to_external_offer(
candidate: dict[str, Any],
*,
@@ -839,7 +864,7 @@ def sync_targeted_momo_candidates_to_external_offers(
_ensure_external_market_source_seeds(conn)
base_observed_at = datetime.now()
offers: list[dict[str, Any]] = []
ranked_offers: list[tuple[dict[str, Any], tuple[float, float, float, float]]] = []
skipped_reasons: dict[str, int] = {}
for index, candidate in enumerate(candidates):
offer, reason = _targeted_candidate_to_external_offer(
@@ -847,9 +872,16 @@ def sync_targeted_momo_candidates_to_external_offers(
observed_at=base_observed_at + timedelta(microseconds=index),
)
if offer:
offers.append(offer)
ranked_offers.append((offer, _targeted_candidate_sync_rank(candidate)))
else:
skipped_reasons[reason] = skipped_reasons.get(reason, 0) + 1
selected_by_pchome: dict[str, tuple[dict[str, Any], tuple[float, float, float, float]]] = {}
for offer, rank in ranked_offers:
key = str(offer.get("pchome_product_id") or offer.get("source_offer_key") or "").strip()
existing = selected_by_pchome.get(key)
if existing is None or rank > existing[1]:
selected_by_pchome[key] = (offer, rank)
offers = [offer for offer, _ in selected_by_pchome.values()]
if not dry_run:
for offer in offers:

View File

@@ -361,6 +361,74 @@ def test_sync_targeted_momo_candidates_skips_total_price_identity_review(monkeyp
assert count == 0
def test_sync_targeted_momo_candidates_keeps_best_unit_quantity_match(monkeypatch):
from services import external_market_offer_service as service
monkeypatch.setattr(service, "mark_pchome_growth_cache_stale", lambda: None)
engine = create_engine("sqlite:///:memory:")
_seed_external_offer_sync_tables(engine)
payload = service.sync_targeted_momo_candidates_to_external_offers(engine, [
{
"product_id": "MOMO-SINGLE",
"name": "雪之上 全效合一水凝霜 80g/瓶",
"price": 1380,
"target_pchome_product_id": "PCH-YUKINOUE",
"target_pchome_name": "雪之上 全效合一水凝霜 80g 3入組",
"target_pchome_price": 2960,
"target_match_score": 0.74,
"auto_compare_type": "unit_price",
"target_price_basis": "unit_price",
"target_match_reasons": ["count_conflict", "unit_comparable"],
"target_comparison_mode": "unit_comparable",
"target_unit_price_comparison": {
"comparable": True,
"unit_label": "g",
"momo_total_quantity": 80,
"competitor_total_quantity": 240,
"momo_unit_price": 17.25,
"competitor_unit_price": 12.33,
"unit_gap_pct": 39.86,
},
},
{
"product_id": "MOMO-THREE",
"name": "雪之上 全效合一水凝霜 80g X 3入瓶裝",
"price": 2680,
"target_pchome_product_id": "PCH-YUKINOUE",
"target_pchome_name": "雪之上 全效合一水凝霜 80g 3入組",
"target_pchome_price": 2960,
"target_match_score": 0.74,
"auto_compare_type": "unit_price",
"target_price_basis": "unit_price",
"target_match_reasons": ["unit_comparable"],
"target_comparison_mode": "unit_comparable",
"target_unit_price_comparison": {
"comparable": True,
"unit_label": "g",
"momo_total_quantity": 240,
"competitor_total_quantity": 240,
"momo_unit_price": 11.17,
"competitor_unit_price": 12.33,
"unit_gap_pct": -9.46,
},
},
])
assert payload["success"] is True
assert payload["candidate_count"] == 2
assert payload["written_count"] == 1
with engine.connect() as conn:
row = conn.execute(text("""
SELECT source_product_id, raw_payload_json
FROM external_offers
""")).mappings().one()
raw_payload = __import__("json").loads(row["raw_payload_json"])
assert row["source_product_id"] == "MOMO-THREE"
assert raw_payload["unit_price_comparison"]["momo_total_quantity"] == 240
def test_external_source_readiness_uses_legacy_momo_reference_cache():
from services.external_market_offer_service import build_external_source_readiness