feat: schedule growth momo backfill
All checks were successful
CD Pipeline / deploy (push) Successful in 1m11s

This commit is contained in:
OoO
2026-06-19 00:18:53 +08:00
parent 9ca8d4e43c
commit 4c59b74ced
11 changed files with 315 additions and 156 deletions

View File

@@ -77,7 +77,7 @@ def _store_action_memory(
ALLOWED_RETRY_TASKS = {
"run_auto_import_task", "run_momo_task", "run_edm_task",
"run_competitor_price_feeder_task", "run_backup_monitor_task",
"run_pchome_match_backfill_task",
"run_pchome_match_backfill_task", "run_pchome_growth_momo_backfill_task",
"run_icaim_analysis_task", "run_festival_task", "run_whitepage_check",
"run_icaim_analysis_task", "run_db_backup_task", "run_promo_event_task",
}

View File

@@ -0,0 +1,165 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""PChome 高業績商品主動反查 MOMO 候選,寫入外部報價層。"""
from __future__ import annotations
from typing import Any, Callable
def candidate_auto_compare_type(candidate: dict[str, Any]) -> str:
auto_type = str(candidate.get("auto_compare_type") or "").strip()
if auto_type in {"total_price", "unit_price"}:
return auto_type
if candidate.get("can_auto_compare") is True:
return "total_price"
return "manual_review"
def build_momo_backfill_targets(payload: dict[str, Any], limit: int) -> list[dict[str, Any]]:
opportunities = list((payload or {}).get("opportunities") or [])
targets: list[dict[str, Any]] = []
for item in opportunities:
action = item.get("recommended_action") or {}
if item.get("external_price"):
continue
if action.get("code") != "map_external_product":
continue
product_id = str(item.get("pchome_product_id") or "").strip()
product_name = str(item.get("product_name") or "").strip()
if not product_id or not product_name:
continue
targets.append({
"product_id": product_id,
"name": product_name,
"price": item.get("pchome_price"),
"sales_7d": item.get("sales_7d"),
"priority_score": item.get("priority_score"),
})
if len(targets) >= limit:
break
return targets
def _default_build_payload(engine, limit: int) -> dict[str, Any]:
from services.pchome_revenue_growth_service import build_pchome_growth_opportunities
return build_pchome_growth_opportunities(engine, limit=limit)
def _default_search_candidates(targets: list[dict[str, Any]], limit: int):
from services.momo_crawler import search_momo_products_for_pchome_products
return search_momo_products_for_pchome_products(
targets,
max_products=limit,
limit_per_product=6,
max_terms_per_product=4,
min_score=0.45,
)
def _default_sync_candidates(engine, candidates: list[dict[str, Any]]) -> dict[str, Any]:
from services.external_market_offer_service import sync_targeted_momo_candidates_to_external_offers
return sync_targeted_momo_candidates_to_external_offers(engine, candidates, dry_run=False)
def run_pchome_growth_momo_backfill(
engine,
*,
limit: int = 12,
build_payload_func: Callable[[Any, int], dict[str, Any]] | None = None,
search_func: Callable[[list[dict[str, Any]], int], tuple[bool, str, list[dict[str, Any]]]] | None = None,
sync_func: Callable[[Any, list[dict[str, Any]]], dict[str, Any]] | None = None,
) -> dict[str, Any]:
"""補高業績 PChome 商品的 MOMO 對應。
不呼叫 LLM只搜尋 MOMO 候選,並只把可自動判斷的 total_price / unit_price
寫入 external_offers需人工確認的候選只回報、不寫入。
"""
limit = max(1, min(int(limit or 12), 20))
build_payload = build_payload_func or _default_build_payload
search_candidates = search_func or _default_search_candidates
sync_candidates = sync_func or _default_sync_candidates
before_payload = build_payload(engine, max(limit, 16))
targets = build_momo_backfill_targets(before_payload, limit)
if not targets:
return {
"success": True,
"message": "目前高業績清單沒有需要補 MOMO 對應的商品。",
"data": {
"scanned_products": 0,
"target_count": 0,
"candidate_count": 0,
"exact_compare_count": 0,
"unit_compare_count": 0,
"auto_compare_count": 0,
"review_count": 0,
"external_offer_sync": {
"success": True,
"status": "not_needed",
"written_count": 0,
"message": "沒有需要同步的自動候選。",
},
"before_stats": before_payload.get("stats") or {},
"after_stats": before_payload.get("stats") or {},
"targets": [],
"review_candidates": [],
},
}
search_success, search_message, candidates = search_candidates(targets, limit)
candidates = list(candidates or [])
exact_candidates = [
item for item in candidates
if candidate_auto_compare_type(item) == "total_price"
]
unit_candidates = [
item for item in candidates
if candidate_auto_compare_type(item) == "unit_price"
]
review_candidates = [
item for item in candidates
if candidate_auto_compare_type(item) not in {"total_price", "unit_price"}
]
auto_candidates = [*exact_candidates, *unit_candidates]
external_offer_sync = {
"success": True,
"status": "not_found",
"written_count": 0,
"message": "已搜尋 MOMO但尚未找到可自動寫入的同款或單位價候選。",
}
if auto_candidates:
external_offer_sync = sync_candidates(engine, auto_candidates)
after_payload = build_payload(engine, max(limit, 16))
written_count = int(external_offer_sync.get("written_count") or 0)
message = (
f"已掃描 {len(targets)} 個高業績商品,找到 {len(candidates)} 筆 MOMO 候選,"
f"自動寫入 {written_count} 筆。"
)
if not search_success and not candidates:
message = search_message or "已搜尋 MOMO但沒有找到可用候選。"
return {
"success": True,
"message": message,
"data": {
"search_success": bool(search_success),
"search_message": search_message,
"scanned_products": len(targets),
"target_count": len(targets),
"candidate_count": len(candidates),
"exact_compare_count": len(exact_candidates),
"unit_compare_count": len(unit_candidates),
"auto_compare_count": len(auto_candidates),
"review_count": len(review_candidates),
"external_offer_sync": external_offer_sync,
"before_stats": before_payload.get("stats") or {},
"after_stats": after_payload.get("stats") or {},
"targets": targets[:8],
"review_candidates": review_candidates[:8],
},
}