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:
@@ -402,7 +402,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
|
|||||||
# ==========================================
|
# ==========================================
|
||||||
# 系統版本與路徑
|
# 系統版本與路徑
|
||||||
# ==========================================
|
# ==========================================
|
||||||
SYSTEM_VERSION = "V10.634"
|
SYSTEM_VERSION = "V10.635"
|
||||||
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
|
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
|
||||||
public_url = PUBLIC_URL # 用於模板顯示
|
public_url = PUBLIC_URL # 用於模板顯示
|
||||||
|
|
||||||
|
|||||||
@@ -1662,63 +1662,10 @@ def api_pchome_growth_opportunities():
|
|||||||
}), 500
|
}), 500
|
||||||
|
|
||||||
|
|
||||||
def _growth_candidate_auto_compare_type(candidate):
|
def _run_pchome_growth_momo_backfill(engine, limit):
|
||||||
auto_type = str(candidate.get("auto_compare_type") or "").strip()
|
from services.pchome_growth_momo_backfill_service import run_pchome_growth_momo_backfill
|
||||||
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"
|
|
||||||
|
|
||||||
|
return run_pchome_growth_momo_backfill(engine, limit=limit)
|
||||||
def _growth_momo_backfill_targets_from_payload(payload, limit):
|
|
||||||
opportunities = list((payload or {}).get("opportunities") or [])
|
|
||||||
targets = []
|
|
||||||
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
|
|
||||||
target = {
|
|
||||||
"product_id": product_id,
|
|
||||||
"name": product_name,
|
|
||||||
"price": item.get("pchome_price"),
|
|
||||||
"sales_7d": item.get("sales_7d"),
|
|
||||||
"priority_score": item.get("priority_score"),
|
|
||||||
}
|
|
||||||
targets.append(target)
|
|
||||||
if len(targets) >= limit:
|
|
||||||
break
|
|
||||||
return targets
|
|
||||||
|
|
||||||
|
|
||||||
def _build_pchome_growth_payload(engine, limit):
|
|
||||||
from services.pchome_revenue_growth_service import build_pchome_growth_opportunities
|
|
||||||
|
|
||||||
return build_pchome_growth_opportunities(engine, limit=limit)
|
|
||||||
|
|
||||||
|
|
||||||
def _search_growth_momo_candidates(targets, limit):
|
|
||||||
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 _sync_growth_momo_candidates(engine, candidates):
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
@ai_bp.route('/api/ai/pchome-growth/backfill-momo-candidates', methods=['POST'])
|
@ai_bp.route('/api/ai/pchome-growth/backfill-momo-candidates', methods=['POST'])
|
||||||
@@ -1736,89 +1683,13 @@ def api_pchome_growth_backfill_momo_candidates():
|
|||||||
from config import DATABASE_PATH
|
from config import DATABASE_PATH
|
||||||
|
|
||||||
engine = _create_icaim_dashboard_engine(DATABASE_PATH)
|
engine = _create_icaim_dashboard_engine(DATABASE_PATH)
|
||||||
before_payload = _build_pchome_growth_payload(engine, limit=max(limit, 16))
|
result = _run_pchome_growth_momo_backfill(engine, limit)
|
||||||
targets = _growth_momo_backfill_targets_from_payload(before_payload, limit)
|
|
||||||
if not targets:
|
|
||||||
return jsonify({
|
|
||||||
"success": True,
|
|
||||||
"message": "目前高業績清單沒有需要補 MOMO 對應的商品。",
|
|
||||||
"data": {
|
|
||||||
"scanned_products": 0,
|
|
||||||
"target_count": 0,
|
|
||||||
"candidate_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": [],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
search_success, search_message, candidates = _search_growth_momo_candidates(targets, limit)
|
|
||||||
candidates = list(candidates or [])
|
|
||||||
exact_candidates = [
|
|
||||||
item for item in candidates
|
|
||||||
if _growth_candidate_auto_compare_type(item) == "total_price"
|
|
||||||
]
|
|
||||||
unit_candidates = [
|
|
||||||
item for item in candidates
|
|
||||||
if _growth_candidate_auto_compare_type(item) == "unit_price"
|
|
||||||
]
|
|
||||||
review_candidates = [
|
|
||||||
item for item in candidates
|
|
||||||
if _growth_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_growth_momo_candidates(engine, auto_candidates)
|
|
||||||
|
|
||||||
after_payload = _build_pchome_growth_payload(engine, limit=max(limit, 16))
|
|
||||||
_PCHOME_GROWTH_CACHE.update({
|
_PCHOME_GROWTH_CACHE.update({
|
||||||
"expires_at": 0.0,
|
"expires_at": 0.0,
|
||||||
"epoch": 0.0,
|
"epoch": 0.0,
|
||||||
"payload": None,
|
"payload": None,
|
||||||
})
|
})
|
||||||
|
return jsonify(result)
|
||||||
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 jsonify({
|
|
||||||
"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],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.error("[PChomeGrowth] MOMO 對應補抓失敗: %s", exc, exc_info=True)
|
logger.error("[PChomeGrowth] MOMO 對應補抓失敗: %s", exc, exc_info=True)
|
||||||
return jsonify({
|
return jsonify({
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ run_scheduler.py — momo-scheduler 容器入口點
|
|||||||
每 6 小時:quality_rescore、action_plan_hygiene
|
每 6 小時:quality_rescore、action_plan_hygiene
|
||||||
每 12 小時:dedup_batch
|
每 12 小時:dedup_batch
|
||||||
每 10 分鐘:ppt_auto_generation_catchup(補跑被長任務卡過的定期簡報)
|
每 10 分鐘:ppt_auto_generation_catchup(補跑被長任務卡過的定期簡報)
|
||||||
每 1 天 :db_backup(03:00)、cleanup_agent_context(03:30)、backup_monitor(04:00)、daily_report(09:00)、roi_monthly_report gate(09:05)、ai_smoke_summary(09:10)、observability_daily_summary(09:30)、pchome_match_backfill(10:30)、openclaw_meta_analysis(12:00, Phase 4 降頻)、ppt_auto_generation_daily(20:30)、ppt_vision_audit(22:00)、daily_token_report(23:55)
|
每 1 天 :db_backup(03:00)、cleanup_agent_context(03:30)、backup_monitor(04:00)、daily_report(09:00)、roi_monthly_report gate(09:05)、ai_smoke_summary(09:10)、observability_daily_summary(09:30)、pchome_match_backfill(10:30)、pchome_growth_momo_backfill(10:45)、openclaw_meta_analysis(12:00, Phase 4 降頻)、ppt_auto_generation_daily(20:30)、ppt_vision_audit(22:00)、daily_token_report(23:55)
|
||||||
每 1 週 :weekly_strategy(週一 06:00)、ppt_auto_generation_weekly(週一 20:40)
|
每 1 週 :weekly_strategy(週一 06:00)、ppt_auto_generation_weekly(週一 20:40)
|
||||||
每 1 月 :monthly_report(每月1日 07:00)、ppt_auto_generation_monthly(每月1日 20:50)
|
每 1 月 :monthly_report(每月1日 07:00)、ppt_auto_generation_monthly(每月1日 20:50)
|
||||||
每 1 季 :ppt_auto_generation_quarterly(1/4/7/10 月 1 日 21:00)
|
每 1 季 :ppt_auto_generation_quarterly(1/4/7/10 月 1 日 21:00)
|
||||||
@@ -36,6 +36,7 @@ from scheduler import (
|
|||||||
run_competitor_price_feeder_task,
|
run_competitor_price_feeder_task,
|
||||||
run_external_offer_sync_task,
|
run_external_offer_sync_task,
|
||||||
run_pchome_match_backfill_task,
|
run_pchome_match_backfill_task,
|
||||||
|
run_pchome_growth_momo_backfill_task,
|
||||||
run_icaim_analysis_task,
|
run_icaim_analysis_task,
|
||||||
run_weekly_strategy_task,
|
run_weekly_strategy_task,
|
||||||
run_db_backup_task,
|
run_db_backup_task,
|
||||||
@@ -308,6 +309,9 @@ def _register_schedules():
|
|||||||
schedule.every().day.at("10:30").do(run_pchome_match_backfill_task)
|
schedule.every().day.at("10:30").do(run_pchome_match_backfill_task)
|
||||||
logger.info("📅 每日 10:30:pchome_match_backfill")
|
logger.info("📅 每日 10:30:pchome_match_backfill")
|
||||||
|
|
||||||
|
schedule.every().day.at("10:45").do(run_pchome_growth_momo_backfill_task)
|
||||||
|
logger.info("📅 每日 10:45:pchome_growth_momo_backfill(高業績商品補 MOMO 對應)")
|
||||||
|
|
||||||
# Operation Ollama-First v5.0 — Phase 1 收尾:每日 23:55 LLM Token 日報
|
# Operation Ollama-First v5.0 — Phase 1 收尾:每日 23:55 LLM Token 日報
|
||||||
schedule.every().day.at("23:55").do(run_daily_token_report_task)
|
schedule.every().day.at("23:55").do(run_daily_token_report_task)
|
||||||
logger.info("📅 每日 23:55:daily_token_report")
|
logger.info("📅 每日 23:55:daily_token_report")
|
||||||
|
|||||||
74
scheduler.py
74
scheduler.py
@@ -2408,6 +2408,80 @@ def run_pchome_match_backfill_task():
|
|||||||
logging.error(f"[Scheduler] [PChomeBackfill] event_router 失敗: {_router_e}")
|
logging.error(f"[Scheduler] [PChomeBackfill] event_router 失敗: {_router_e}")
|
||||||
|
|
||||||
|
|
||||||
|
def run_pchome_growth_momo_backfill_task():
|
||||||
|
"""
|
||||||
|
PChome 高業績商品 MOMO 對應補齊任務(每日執行)
|
||||||
|
優先處理成長作戰台中尚未有外部比價資料的高業績商品,寫入 external_offers。
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from config import DATABASE_PATH
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
from services.pchome_growth_momo_backfill_service import run_pchome_growth_momo_backfill
|
||||||
|
|
||||||
|
now_str = datetime.now(TAIPEI_TZ).strftime('%Y-%m-%d %H:%M')
|
||||||
|
limit = int(os.getenv("PCHOME_GROWTH_MOMO_BACKFILL_LIMIT", "8"))
|
||||||
|
logging.info(
|
||||||
|
"[Scheduler] [PChomeGrowthMomoBackfill] 🚀 啟動高業績商品 MOMO 對應補齊 | %s | limit=%s",
|
||||||
|
now_str,
|
||||||
|
limit,
|
||||||
|
)
|
||||||
|
|
||||||
|
engine = create_engine(DATABASE_PATH)
|
||||||
|
try:
|
||||||
|
result = run_pchome_growth_momo_backfill(engine, limit=limit)
|
||||||
|
finally:
|
||||||
|
engine.dispose()
|
||||||
|
|
||||||
|
data = result.get("data") or {}
|
||||||
|
before_stats = data.get("before_stats") or {}
|
||||||
|
after_stats = data.get("after_stats") or {}
|
||||||
|
sync_result = data.get("external_offer_sync") or {}
|
||||||
|
stats = {
|
||||||
|
"status": "Success" if result.get("success") else "Skipped",
|
||||||
|
"scanned_products": data.get("scanned_products", 0),
|
||||||
|
"candidate_count": data.get("candidate_count", 0),
|
||||||
|
"auto_compare_count": data.get("auto_compare_count", 0),
|
||||||
|
"review_count": data.get("review_count", 0),
|
||||||
|
"written_count": sync_result.get("written_count", 0),
|
||||||
|
"sync_status": sync_result.get("status"),
|
||||||
|
"mapping_rate_before": before_stats.get("mapping_rate"),
|
||||||
|
"mapping_rate_after": after_stats.get("mapping_rate"),
|
||||||
|
"mapped_count_before": before_stats.get("mapped_count"),
|
||||||
|
"mapped_count_after": after_stats.get("mapped_count"),
|
||||||
|
"needs_mapping_count_after": after_stats.get("needs_mapping_count"),
|
||||||
|
"message": result.get("message"),
|
||||||
|
}
|
||||||
|
logging.info(
|
||||||
|
"[Scheduler] [PChomeGrowthMomoBackfill] ✅ 完成 | scanned=%s candidates=%s auto=%s written=%s review=%s mapping=%s%%→%s%%",
|
||||||
|
stats["scanned_products"],
|
||||||
|
stats["candidate_count"],
|
||||||
|
stats["auto_compare_count"],
|
||||||
|
stats["written_count"],
|
||||||
|
stats["review_count"],
|
||||||
|
stats["mapping_rate_before"],
|
||||||
|
stats["mapping_rate_after"],
|
||||||
|
)
|
||||||
|
_save_stats('pchome_growth_momo_backfill', stats)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
import traceback as _tb
|
||||||
|
logging.error(f"[Scheduler] [PChomeGrowthMomoBackfill] 🚨 任務異常 | Error: {e}")
|
||||||
|
_save_stats('pchome_growth_momo_backfill', {"status": "Failed", "error": str(e)})
|
||||||
|
try:
|
||||||
|
from services.event_router import notify_failure
|
||||||
|
notify_failure(
|
||||||
|
task_name="run_pchome_growth_momo_backfill_task",
|
||||||
|
error=e,
|
||||||
|
source="Scheduler.PChomeGrowthMomoBackfill",
|
||||||
|
event_type="pchome_growth_momo_backfill_failure",
|
||||||
|
priority="P2",
|
||||||
|
title="PChome 高業績商品 MOMO 對應補齊異常",
|
||||||
|
trace=_tb.format_exc(),
|
||||||
|
)
|
||||||
|
except Exception as _router_e:
|
||||||
|
logging.error(f"[Scheduler] [PChomeGrowthMomoBackfill] event_router 失敗: {_router_e}")
|
||||||
|
|
||||||
|
|
||||||
def run_icaim_analysis_task():
|
def run_icaim_analysis_task():
|
||||||
"""
|
"""
|
||||||
ICAIM 競價情報分析排程任務(每 6 小時執行一次)
|
ICAIM 競價情報分析排程任務(每 6 小時執行一次)
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ def _store_action_memory(
|
|||||||
ALLOWED_RETRY_TASKS = {
|
ALLOWED_RETRY_TASKS = {
|
||||||
"run_auto_import_task", "run_momo_task", "run_edm_task",
|
"run_auto_import_task", "run_momo_task", "run_edm_task",
|
||||||
"run_competitor_price_feeder_task", "run_backup_monitor_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_festival_task", "run_whitepage_check",
|
||||||
"run_icaim_analysis_task", "run_db_backup_task", "run_promo_event_task",
|
"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],
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -99,7 +99,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
<div class="growth-backfill-status momo-mono" data-pchome-growth-backfill-status>
|
<div class="growth-backfill-status momo-mono" data-pchome-growth-backfill-status>
|
||||||
按下後會優先補高業績商品的 MOMO 對應
|
每日 10:45 自動補對應;按下可立即補高業績商品的 MOMO 對應
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -385,5 +385,9 @@ def test_external_offer_sync_is_registered_in_scheduler():
|
|||||||
|
|
||||||
assert "def run_external_offer_sync_task" in scheduler_source
|
assert "def run_external_offer_sync_task" in scheduler_source
|
||||||
assert "sync_legacy_momo_reference_offers" in scheduler_source
|
assert "sync_legacy_momo_reference_offers" in scheduler_source
|
||||||
|
assert "def run_pchome_growth_momo_backfill_task" in scheduler_source
|
||||||
|
assert "run_pchome_growth_momo_backfill" in scheduler_source
|
||||||
assert "run_external_offer_sync_task" in run_scheduler_source
|
assert "run_external_offer_sync_task" in run_scheduler_source
|
||||||
|
assert "run_pchome_growth_momo_backfill_task" in run_scheduler_source
|
||||||
assert "external_offer_sync" in run_scheduler_source
|
assert "external_offer_sync" in run_scheduler_source
|
||||||
|
assert "pchome_growth_momo_backfill" in run_scheduler_source
|
||||||
|
|||||||
@@ -899,11 +899,16 @@ def test_ai_product_pick_agent_uses_real_competitor_data_and_dashboard_action():
|
|||||||
run_scheduler_source = (ROOT / "run_scheduler.py").read_text(encoding="utf-8")
|
run_scheduler_source = (ROOT / "run_scheduler.py").read_text(encoding="utf-8")
|
||||||
agent_actions_source = (ROOT / "services/agent_actions.py").read_text(encoding="utf-8")
|
agent_actions_source = (ROOT / "services/agent_actions.py").read_text(encoding="utf-8")
|
||||||
assert "def run_pchome_match_backfill_task" in scheduler_source
|
assert "def run_pchome_match_backfill_task" in scheduler_source
|
||||||
|
assert "def run_pchome_growth_momo_backfill_task" in scheduler_source
|
||||||
assert "_save_stats('pchome_match_backfill'" in scheduler_source
|
assert "_save_stats('pchome_match_backfill'" in scheduler_source
|
||||||
|
assert "_save_stats('pchome_growth_momo_backfill'" in scheduler_source
|
||||||
assert "retryable_candidate_revalidation_total" in scheduler_source
|
assert "retryable_candidate_revalidation_total" in scheduler_source
|
||||||
assert "run_pchome_match_backfill_task" in run_scheduler_source
|
assert "run_pchome_match_backfill_task" in run_scheduler_source
|
||||||
|
assert "run_pchome_growth_momo_backfill_task" in run_scheduler_source
|
||||||
assert "每日 10:30:pchome_match_backfill" in run_scheduler_source
|
assert "每日 10:30:pchome_match_backfill" in run_scheduler_source
|
||||||
|
assert "每日 10:45:pchome_growth_momo_backfill" in run_scheduler_source
|
||||||
assert '"run_pchome_match_backfill_task"' in agent_actions_source
|
assert '"run_pchome_match_backfill_task"' in agent_actions_source
|
||||||
|
assert '"run_pchome_growth_momo_backfill_task"' in agent_actions_source
|
||||||
|
|
||||||
assert "產生今日清單" in template
|
assert "產生今日清單" in template
|
||||||
assert "補齊比價資料" in template
|
assert "補齊比價資料" in template
|
||||||
@@ -917,6 +922,7 @@ def test_ai_product_pick_agent_uses_real_competitor_data_and_dashboard_action():
|
|||||||
assert "PChome 比價補強" in dashboard_template
|
assert "PChome 比價補強" in dashboard_template
|
||||||
assert "data-pchome-growth-backfill-trigger" in dashboard_template
|
assert "data-pchome-growth-backfill-trigger" in dashboard_template
|
||||||
assert "data-pchome-growth-backfill-status" in dashboard_template
|
assert "data-pchome-growth-backfill-status" in dashboard_template
|
||||||
|
assert "每日 10:45 自動補對應" in dashboard_template
|
||||||
assert "PCHOME MATCH BACKFILL" not in dashboard_template
|
assert "PCHOME MATCH BACKFILL" not in dashboard_template
|
||||||
assert ">ACTIVE<" not in dashboard_template
|
assert ">ACTIVE<" not in dashboard_template
|
||||||
assert "目前 ACTIVE 商品" not in dashboard_template
|
assert "目前 ACTIVE 商品" not in dashboard_template
|
||||||
|
|||||||
@@ -225,15 +225,13 @@ def test_pchome_growth_route_cache_respects_shared_invalidation_epoch(monkeypatc
|
|||||||
assert routes._get_cached_pchome_growth_payload() is None
|
assert routes._get_cached_pchome_growth_payload() is None
|
||||||
|
|
||||||
|
|
||||||
def test_pchome_growth_momo_backfill_route_targets_unmapped_high_sales_items(monkeypatch):
|
def test_pchome_growth_momo_backfill_service_targets_unmapped_high_sales_items():
|
||||||
from flask import Flask
|
from services.pchome_growth_momo_backfill_service import run_pchome_growth_momo_backfill
|
||||||
from routes import ai_routes as routes
|
|
||||||
|
|
||||||
captured = {}
|
captured = {}
|
||||||
|
|
||||||
class FakeEngine:
|
class FakeEngine:
|
||||||
def dispose(self):
|
pass
|
||||||
captured["disposed"] = True
|
|
||||||
|
|
||||||
before_payload = {
|
before_payload = {
|
||||||
"success": True,
|
"success": True,
|
||||||
@@ -309,20 +307,14 @@ def test_pchome_growth_momo_backfill_route_targets_unmapped_high_sales_items(mon
|
|||||||
"unit_price_count": 1,
|
"unit_price_count": 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
monkeypatch.setattr(routes, "_create_icaim_dashboard_engine", lambda database_path: FakeEngine())
|
payload = run_pchome_growth_momo_backfill(
|
||||||
monkeypatch.setattr(routes, "_build_pchome_growth_payload", fake_build_payload)
|
FakeEngine(),
|
||||||
monkeypatch.setattr(routes, "_search_growth_momo_candidates", fake_search)
|
limit=2,
|
||||||
monkeypatch.setattr(routes, "_sync_growth_momo_candidates", fake_sync)
|
build_payload_func=fake_build_payload,
|
||||||
|
search_func=fake_search,
|
||||||
|
sync_func=fake_sync,
|
||||||
|
)
|
||||||
|
|
||||||
app = Flask(__name__)
|
|
||||||
with app.test_request_context(
|
|
||||||
"/api/ai/pchome-growth/backfill-momo-candidates",
|
|
||||||
method="POST",
|
|
||||||
json={"limit": 2},
|
|
||||||
):
|
|
||||||
response = routes.api_pchome_growth_backfill_momo_candidates.__wrapped__()
|
|
||||||
|
|
||||||
payload = response.get_json()
|
|
||||||
assert payload["success"] is True
|
assert payload["success"] is True
|
||||||
assert payload["data"]["scanned_products"] == 2
|
assert payload["data"]["scanned_products"] == 2
|
||||||
assert payload["data"]["candidate_count"] == 3
|
assert payload["data"]["candidate_count"] == 3
|
||||||
@@ -334,6 +326,47 @@ def test_pchome_growth_momo_backfill_route_targets_unmapped_high_sales_items(mon
|
|||||||
assert [item["price"] for item in captured["targets"]] == [920, 760]
|
assert [item["price"] for item in captured["targets"]] == [920, 760]
|
||||||
assert [item["product_id"] for item in captured["sync_candidates"]] == ["MOMO-AUTO", "MOMO-UNIT"]
|
assert [item["product_id"] for item in captured["sync_candidates"]] == ["MOMO-AUTO", "MOMO-UNIT"]
|
||||||
assert captured["search_limit"] == 2
|
assert captured["search_limit"] == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_pchome_growth_momo_backfill_route_calls_shared_service(monkeypatch):
|
||||||
|
from flask import Flask
|
||||||
|
from routes import ai_routes as routes
|
||||||
|
|
||||||
|
captured = {}
|
||||||
|
|
||||||
|
class FakeEngine:
|
||||||
|
def dispose(self):
|
||||||
|
captured["disposed"] = True
|
||||||
|
|
||||||
|
def fake_run(engine, limit):
|
||||||
|
captured["limit"] = limit
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": "已完成",
|
||||||
|
"data": {
|
||||||
|
"scanned_products": 2,
|
||||||
|
"candidate_count": 3,
|
||||||
|
"auto_compare_count": 2,
|
||||||
|
"review_count": 1,
|
||||||
|
"external_offer_sync": {"written_count": 2},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
monkeypatch.setattr(routes, "_create_icaim_dashboard_engine", lambda database_path: FakeEngine())
|
||||||
|
monkeypatch.setattr(routes, "_run_pchome_growth_momo_backfill", fake_run)
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
with app.test_request_context(
|
||||||
|
"/api/ai/pchome-growth/backfill-momo-candidates",
|
||||||
|
method="POST",
|
||||||
|
json={"limit": 2},
|
||||||
|
):
|
||||||
|
response = routes.api_pchome_growth_backfill_momo_candidates.__wrapped__()
|
||||||
|
|
||||||
|
payload = response.get_json()
|
||||||
|
assert payload["success"] is True
|
||||||
|
assert payload["data"]["external_offer_sync"]["written_count"] == 2
|
||||||
|
assert captured["limit"] == 2
|
||||||
assert captured["disposed"] is True
|
assert captured["disposed"] is True
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -226,6 +226,8 @@ def test_roi_ai_smoke_and_daily_report_schedules_stay_staggered():
|
|||||||
assert 'schedule.every().day.at("09:00").do(run_daily_report_task)' in source
|
assert 'schedule.every().day.at("09:00").do(run_daily_report_task)' in source
|
||||||
assert 'schedule.every().day.at("09:05").do(run_roi_monthly_report_if_new_month)' in source
|
assert 'schedule.every().day.at("09:05").do(run_roi_monthly_report_if_new_month)' in source
|
||||||
assert 'schedule.every().day.at("09:10").do(run_ai_smoke_daily_summary_task)' in source
|
assert 'schedule.every().day.at("09:10").do(run_ai_smoke_daily_summary_task)' in source
|
||||||
|
assert 'schedule.every().day.at("10:30").do(run_pchome_match_backfill_task)' in source
|
||||||
|
assert 'schedule.every().day.at("10:45").do(run_pchome_growth_momo_backfill_task)' in source
|
||||||
assert "schedule.every(6).hours.do(run_action_plan_hygiene_task)" in source
|
assert "schedule.every(6).hours.do(run_action_plan_hygiene_task)" in source
|
||||||
assert "schedule.every(15).minutes.do(run_ollama_111_usage_guard_check)" in source
|
assert "schedule.every(15).minutes.do(run_ollama_111_usage_guard_check)" in source
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user