feat: schedule growth momo backfill
All checks were successful
CD Pipeline / deploy (push) Successful in 1m11s
All checks were successful
CD Pipeline / deploy (push) Successful in 1m11s
This commit is contained in:
@@ -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",
|
||||
}
|
||||
|
||||
165
services/pchome_growth_momo_backfill_service.py
Normal file
165
services/pchome_growth_momo_backfill_service.py
Normal 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],
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user