|
@@ -16,15 +16,18 @@ from operator_commands import (
|
|
|
from storage import (
|
|
from storage import (
|
|
|
advisory_lock,
|
|
advisory_lock,
|
|
|
clear_operator_pause,
|
|
clear_operator_pause,
|
|
|
- create_operator_command,
|
|
|
|
|
|
|
+ create_operator_command_with_items,
|
|
|
|
|
+ find_pending_command_conflict,
|
|
|
insert_operator_command_item,
|
|
insert_operator_command_item,
|
|
|
load_ad_states,
|
|
load_ad_states,
|
|
|
- load_managed_accounts,
|
|
|
|
|
|
|
+ load_operator_command_items,
|
|
|
load_operator_command,
|
|
load_operator_command,
|
|
|
load_operator_pauses,
|
|
load_operator_pauses,
|
|
|
|
|
+ load_realtime_accounts,
|
|
|
set_operator_pause,
|
|
set_operator_pause,
|
|
|
transition_operator_command,
|
|
transition_operator_command,
|
|
|
update_operator_command,
|
|
update_operator_command,
|
|
|
|
|
+ update_operator_command_item,
|
|
|
upsert_ad_state,
|
|
upsert_ad_state,
|
|
|
)
|
|
)
|
|
|
from tencent_client import (
|
|
from tencent_client import (
|
|
@@ -63,7 +66,7 @@ logger = logging.getLogger("tencent_realtime_control.operator_control")
|
|
|
def _managed_account_map() -> dict[int, dict[str, Any]]:
|
|
def _managed_account_map() -> dict[int, dict[str, Any]]:
|
|
|
return {
|
|
return {
|
|
|
int(row["account_id"]): row
|
|
int(row["account_id"]): row
|
|
|
- for row in load_managed_accounts()
|
|
|
|
|
|
|
+ for row in load_realtime_accounts()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@@ -72,11 +75,11 @@ def resolve_target_accounts(parsed: ParsedCommand) -> list[dict[str, Any]]:
|
|
|
if parsed.scope_type == "ALL":
|
|
if parsed.scope_type == "ALL":
|
|
|
accounts = list(managed.values())
|
|
accounts = list(managed.values())
|
|
|
if not accounts:
|
|
if not accounts:
|
|
|
- raise ValueError("当前没有启用白名单的自动化管理账户")
|
|
|
|
|
|
|
+ raise ValueError("当前没有纳入实时控制的账户")
|
|
|
return accounts
|
|
return accounts
|
|
|
missing = [account_id for account_id in parsed.account_ids if account_id not in managed]
|
|
missing = [account_id for account_id in parsed.account_ids if account_id not in managed]
|
|
|
if missing:
|
|
if missing:
|
|
|
- raise ValueError(f"账户不在自动化管理范围: {missing}")
|
|
|
|
|
|
|
+ raise ValueError(f"账户不在实时控制范围: {missing}")
|
|
|
return [managed[account_id] for account_id in parsed.account_ids]
|
|
return [managed[account_id] for account_id in parsed.account_ids]
|
|
|
|
|
|
|
|
|
|
|
|
@@ -88,6 +91,38 @@ def _next_delivery_start(now: datetime, start_hour: int) -> datetime:
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+def _load_today_metrics(
|
|
|
|
|
+ client: TencentClient,
|
|
|
|
|
+ account_id: int,
|
|
|
|
|
+ adgroup_ids: list[int],
|
|
|
|
|
+ now: datetime,
|
|
|
|
|
+) -> dict[int, dict[str, int]]:
|
|
|
|
|
+ metrics: dict[int, dict[str, int]] = {}
|
|
|
|
|
+ for start in range(0, len(adgroup_ids), 100):
|
|
|
|
|
+ metrics.update(
|
|
|
|
|
+ client.get_today_ad_metrics(
|
|
|
|
|
+ account_id,
|
|
|
|
|
+ adgroup_ids[start:start + 100],
|
|
|
|
|
+ now.date(),
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ return metrics
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _operator_pause_is_active(state: dict[str, Any], now: datetime) -> bool:
|
|
|
|
|
+ mode = str(state.get("operator_pause_mode") or "")
|
|
|
|
|
+ if mode == PAUSE_UNTIL_MANUAL:
|
|
|
|
|
+ return True
|
|
|
|
|
+ if mode != PAUSE_UNTIL_NEXT_DELIVERY:
|
|
|
|
|
+ return False
|
|
|
|
|
+ resume_at = state.get("operator_resume_at")
|
|
|
|
|
+ if not resume_at:
|
|
|
|
|
+ return False
|
|
|
|
|
+ if resume_at.tzinfo is None:
|
|
|
|
|
+ resume_at = resume_at.replace(tzinfo=now.tzinfo)
|
|
|
|
|
+ return resume_at > now
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
def preview_write_command(
|
|
def preview_write_command(
|
|
|
parsed: ParsedCommand,
|
|
parsed: ParsedCommand,
|
|
|
*,
|
|
*,
|
|
@@ -98,48 +133,194 @@ def preview_write_command(
|
|
|
sender_name: str | None,
|
|
sender_name: str | None,
|
|
|
confirmation_ttl_minutes: int,
|
|
confirmation_ttl_minutes: int,
|
|
|
start_hour: int,
|
|
start_hour: int,
|
|
|
|
|
+ raw_text: str = "",
|
|
|
|
|
+ parse_source: str = "deterministic",
|
|
|
|
|
+ intent: dict[str, Any] | None = None,
|
|
|
|
|
+ preview_lock_name: str = "tencent_operator_command_preview",
|
|
|
tencent: TencentClient | None = None,
|
|
tencent: TencentClient | None = None,
|
|
|
) -> dict[str, Any]:
|
|
) -> dict[str, Any]:
|
|
|
accounts = resolve_target_accounts(parsed)
|
|
accounts = resolve_target_accounts(parsed)
|
|
|
client = tencent or TencentClient()
|
|
client = tencent or TencentClient()
|
|
|
- preview_ad_count = 0
|
|
|
|
|
|
|
+ next_day = (now.date() + timedelta(days=1)).isoformat()
|
|
|
|
|
+ preview_items: list[dict[str, Any]] = []
|
|
|
|
|
+ account_summaries: list[dict[str, Any]] = []
|
|
|
if parsed.action in {ACTION_DAY_PAUSE, ACTION_STOP}:
|
|
if parsed.action in {ACTION_DAY_PAUSE, ACTION_STOP}:
|
|
|
for account in accounts:
|
|
for account in accounts:
|
|
|
- ads = client.get_ads(int(account["account_id"]))
|
|
|
|
|
- preview_ad_count += sum(
|
|
|
|
|
- str(ad.get("configured_status") or "") == ACTIVE_STATUS
|
|
|
|
|
- for ad in ads
|
|
|
|
|
|
|
+ account_id = int(account["account_id"])
|
|
|
|
|
+ operator_pauses = {
|
|
|
|
|
+ int(row["adgroup_id"]): row
|
|
|
|
|
+ for row in load_operator_pauses([account_id])
|
|
|
|
|
+ if _operator_pause_is_active(row, now)
|
|
|
|
|
+ }
|
|
|
|
|
+ ads = [
|
|
|
|
|
+ ad for ad in client.get_ads(account_id)
|
|
|
|
|
+ if (
|
|
|
|
|
+ (
|
|
|
|
|
+ str(ad.get("configured_status") or "") == ACTIVE_STATUS
|
|
|
|
|
+ and (
|
|
|
|
|
+ parsed.action == ACTION_STOP
|
|
|
|
|
+ or not str(ad.get("begin_date") or "")
|
|
|
|
|
+ or str(ad.get("begin_date")) <= now.date().isoformat()
|
|
|
|
|
+ )
|
|
|
|
|
+ and (
|
|
|
|
|
+ parsed.action == ACTION_STOP
|
|
|
|
|
+ or not str(ad.get("end_date") or "")
|
|
|
|
|
+ or str(ad.get("end_date")) >= next_day
|
|
|
|
|
+ )
|
|
|
|
|
+ and (
|
|
|
|
|
+ parsed.action == ACTION_STOP
|
|
|
|
|
+ or int(ad.get("adgroup_id") or 0)
|
|
|
|
|
+ not in operator_pauses
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ or (
|
|
|
|
|
+ parsed.action == ACTION_STOP
|
|
|
|
|
+ and str(ad.get("configured_status") or "")
|
|
|
|
|
+ == SUSPEND_STATUS
|
|
|
|
|
+ and (
|
|
|
|
|
+ operator_pauses.get(
|
|
|
|
|
+ int(ad.get("adgroup_id") or 0),
|
|
|
|
|
+ {},
|
|
|
|
|
+ ).get("operator_pause_mode")
|
|
|
|
|
+ == PAUSE_UNTIL_NEXT_DELIVERY
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ ]
|
|
|
|
|
+ metrics = _load_today_metrics(
|
|
|
|
|
+ client, account_id, [int(ad["adgroup_id"]) for ad in ads], now
|
|
|
)
|
|
)
|
|
|
|
|
+ account_cost = 0
|
|
|
|
|
+ for ad in ads:
|
|
|
|
|
+ adgroup_id = int(ad["adgroup_id"])
|
|
|
|
|
+ values = metrics.get(adgroup_id, {})
|
|
|
|
|
+ account_cost += int(values.get("cost_fen") or 0)
|
|
|
|
|
+ preview_items.append({
|
|
|
|
|
+ "account_id": account_id,
|
|
|
|
|
+ "audience_name": account.get("audience_name"),
|
|
|
|
|
+ "adgroup_id": adgroup_id,
|
|
|
|
|
+ "adgroup_name": ad.get("adgroup_name"),
|
|
|
|
|
+ "before_status": ad.get("configured_status"),
|
|
|
|
|
+ "target_status": (
|
|
|
|
|
+ SUSPEND_STATUS
|
|
|
|
|
+ if parsed.action == ACTION_STOP
|
|
|
|
|
+ else ad.get("configured_status")
|
|
|
|
|
+ ),
|
|
|
|
|
+ "preview_cost_fen": int(values.get("cost_fen") or 0),
|
|
|
|
|
+ "preview_impressions": int(values.get("impressions") or 0),
|
|
|
|
|
+ "preview_clicks": int(values.get("clicks") or 0),
|
|
|
|
|
+ "preview_conversions": int(values.get("conversions") or 0),
|
|
|
|
|
+ "previewed_at": now,
|
|
|
|
|
+ "execution_status": "PREVIEWED",
|
|
|
|
|
+ })
|
|
|
|
|
+ account_summaries.append({
|
|
|
|
|
+ "account_id": account_id,
|
|
|
|
|
+ "ad_count": len(ads),
|
|
|
|
|
+ "cost_fen": account_cost,
|
|
|
|
|
+ })
|
|
|
elif parsed.action == ACTION_RESUME:
|
|
elif parsed.action == ACTION_RESUME:
|
|
|
- preview_ad_count = len(
|
|
|
|
|
- load_operator_pauses([int(row["account_id"]) for row in accounts])
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ pauses_by_account: dict[int, list[dict[str, Any]]] = {}
|
|
|
|
|
+ for state in load_operator_pauses([int(row["account_id"]) for row in accounts]):
|
|
|
|
|
+ if not _operator_pause_is_active(state, now):
|
|
|
|
|
+ continue
|
|
|
|
|
+ pauses_by_account.setdefault(int(state["account_id"]), []).append(state)
|
|
|
|
|
+ for account in accounts:
|
|
|
|
|
+ account_id = int(account["account_id"])
|
|
|
|
|
+ states = pauses_by_account.get(account_id, [])
|
|
|
|
|
+ ads = {
|
|
|
|
|
+ int(ad.get("adgroup_id") or 0): ad
|
|
|
|
|
+ for ad in client.get_ads(account_id)
|
|
|
|
|
+ }
|
|
|
|
|
+ ids = [int(state["adgroup_id"]) for state in states]
|
|
|
|
|
+ metrics = _load_today_metrics(client, account_id, ids, now)
|
|
|
|
|
+ account_cost = 0
|
|
|
|
|
+ for state in states:
|
|
|
|
|
+ adgroup_id = int(state["adgroup_id"])
|
|
|
|
|
+ ad = ads.get(adgroup_id, {})
|
|
|
|
|
+ values = metrics.get(adgroup_id, {})
|
|
|
|
|
+ account_cost += int(values.get("cost_fen") or 0)
|
|
|
|
|
+ preview_items.append({
|
|
|
|
|
+ "account_id": account_id,
|
|
|
|
|
+ "audience_name": account.get("audience_name"),
|
|
|
|
|
+ "adgroup_id": adgroup_id,
|
|
|
|
|
+ "adgroup_name": ad.get("adgroup_name") or state.get("adgroup_name"),
|
|
|
|
|
+ "before_status": ad.get("configured_status"),
|
|
|
|
|
+ "target_status": (
|
|
|
|
|
+ ACTIVE_STATUS
|
|
|
|
|
+ if state.get("operator_pause_mode") == PAUSE_UNTIL_MANUAL
|
|
|
|
|
+ else ad.get("configured_status")
|
|
|
|
|
+ ),
|
|
|
|
|
+ "preview_cost_fen": int(values.get("cost_fen") or 0),
|
|
|
|
|
+ "preview_impressions": int(values.get("impressions") or 0),
|
|
|
|
|
+ "preview_clicks": int(values.get("clicks") or 0),
|
|
|
|
|
+ "preview_conversions": int(values.get("conversions") or 0),
|
|
|
|
|
+ "previewed_at": now,
|
|
|
|
|
+ "execution_status": "PREVIEWED",
|
|
|
|
|
+ })
|
|
|
|
|
+ account_summaries.append({
|
|
|
|
|
+ "account_id": account_id,
|
|
|
|
|
+ "ad_count": len(states),
|
|
|
|
|
+ "cost_fen": account_cost,
|
|
|
|
|
+ })
|
|
|
else:
|
|
else:
|
|
|
raise ValueError(f"Unsupported write action: {parsed.action}")
|
|
raise ValueError(f"Unsupported write action: {parsed.action}")
|
|
|
|
|
|
|
|
|
|
+ if not preview_items:
|
|
|
|
|
+ raise ValueError("当前范围内没有符合操作条件的广告")
|
|
|
|
|
+ impacted_account_ids = sorted({int(row["account_id"]) for row in preview_items})
|
|
|
|
|
+ impacted_accounts = [
|
|
|
|
|
+ account for account in accounts
|
|
|
|
|
+ if int(account["account_id"]) in impacted_account_ids
|
|
|
|
|
+ ]
|
|
|
|
|
+ account_summaries = [
|
|
|
|
|
+ row for row in account_summaries if int(row["account_id"]) in impacted_account_ids
|
|
|
|
|
+ ]
|
|
|
command_id = f"cmd_{now.strftime('%Y%m%d%H%M%S')}_{uuid.uuid4().hex[:8]}"
|
|
command_id = f"cmd_{now.strftime('%Y%m%d%H%M%S')}_{uuid.uuid4().hex[:8]}"
|
|
|
expires_at = now + timedelta(minutes=confirmation_ttl_minutes)
|
|
expires_at = now + timedelta(minutes=confirmation_ttl_minutes)
|
|
|
- command = create_operator_command(
|
|
|
|
|
- {
|
|
|
|
|
- "command_id": command_id,
|
|
|
|
|
- "source_message_id": source_message_id,
|
|
|
|
|
- "chat_id": chat_id,
|
|
|
|
|
- "sender_open_id": sender_open_id,
|
|
|
|
|
- "sender_name": sender_name,
|
|
|
|
|
- "action": parsed.action,
|
|
|
|
|
- "scope_type": parsed.scope_type,
|
|
|
|
|
- "target_account_ids": [int(row["account_id"]) for row in accounts],
|
|
|
|
|
- "status": COMMAND_PENDING,
|
|
|
|
|
- "preview_account_count": len(accounts),
|
|
|
|
|
- "preview_ad_count": preview_ad_count,
|
|
|
|
|
- "expires_at": expires_at,
|
|
|
|
|
- }
|
|
|
|
|
- )
|
|
|
|
|
- command["resume_at"] = (
|
|
|
|
|
- _next_delivery_start(now, start_hour)
|
|
|
|
|
- if parsed.action == ACTION_DAY_PAUSE
|
|
|
|
|
- else None
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ resume_at = _next_delivery_start(now, start_hour) if parsed.action == ACTION_DAY_PAUSE else None
|
|
|
|
|
+ totals = {
|
|
|
|
|
+ "preview_cost_fen": sum(int(row["preview_cost_fen"]) for row in preview_items),
|
|
|
|
|
+ "preview_impressions": sum(int(row["preview_impressions"]) for row in preview_items),
|
|
|
|
|
+ "preview_clicks": sum(int(row["preview_clicks"]) for row in preview_items),
|
|
|
|
|
+ "preview_conversions": sum(int(row["preview_conversions"]) for row in preview_items),
|
|
|
|
|
+ }
|
|
|
|
|
+ for row in preview_items:
|
|
|
|
|
+ row["command_id"] = command_id
|
|
|
|
|
+ command_record = {
|
|
|
|
|
+ "command_id": command_id,
|
|
|
|
|
+ "source_message_id": source_message_id,
|
|
|
|
|
+ "chat_id": chat_id,
|
|
|
|
|
+ "sender_open_id": sender_open_id,
|
|
|
|
|
+ "sender_name": sender_name,
|
|
|
|
|
+ "raw_text": raw_text,
|
|
|
|
|
+ "parse_source": parse_source,
|
|
|
|
|
+ "intent": intent or {},
|
|
|
|
|
+ "action": parsed.action,
|
|
|
|
|
+ "scope_type": parsed.scope_type,
|
|
|
|
|
+ "target_account_ids": impacted_account_ids,
|
|
|
|
|
+ "status": COMMAND_PENDING,
|
|
|
|
|
+ "preview_account_count": len(impacted_accounts),
|
|
|
|
|
+ "preview_ad_count": len(preview_items),
|
|
|
|
|
+ **totals,
|
|
|
|
|
+ "previewed_at": now,
|
|
|
|
|
+ "resume_at": resume_at,
|
|
|
|
|
+ "expires_at": expires_at,
|
|
|
|
|
+ }
|
|
|
|
|
+ with advisory_lock(preview_lock_name) as acquired:
|
|
|
|
|
+ if not acquired:
|
|
|
|
|
+ raise RuntimeError("其他运营命令正在生成预览,请稍后重试")
|
|
|
|
|
+ conflict = find_pending_command_conflict(
|
|
|
|
|
+ [(int(row["account_id"]), int(row["adgroup_id"])) for row in preview_items],
|
|
|
|
|
+ now=now,
|
|
|
|
|
+ )
|
|
|
|
|
+ if conflict:
|
|
|
|
|
+ raise ValueError(
|
|
|
|
|
+ "操作范围与待确认命令冲突: "
|
|
|
|
|
+ f"{conflict['command_id']},账户 {conflict['account_id']},"
|
|
|
|
|
+ f"广告 {conflict['adgroup_id']}"
|
|
|
|
|
+ )
|
|
|
|
|
+ command = create_operator_command_with_items(command_record, preview_items)
|
|
|
|
|
+ command["account_summaries"] = account_summaries
|
|
|
return command
|
|
return command
|
|
|
|
|
|
|
|
|
|
|
|
@@ -200,7 +381,16 @@ def _ensure_state(
|
|
|
return state
|
|
return state
|
|
|
|
|
|
|
|
|
|
|
|
|
-def _record_item(command: dict[str, Any], account: dict[str, Any], **values: Any) -> None:
|
|
|
|
|
|
|
+def _record_item(
|
|
|
|
|
+ command: dict[str, Any],
|
|
|
|
|
+ account: dict[str, Any],
|
|
|
|
|
+ *,
|
|
|
|
|
+ item: dict[str, Any] | None = None,
|
|
|
|
|
+ **values: Any,
|
|
|
|
|
+) -> None:
|
|
|
|
|
+ if item and item.get("id"):
|
|
|
|
|
+ update_operator_command_item(int(item["id"]), **values)
|
|
|
|
|
+ return
|
|
|
insert_operator_command_item(
|
|
insert_operator_command_item(
|
|
|
{
|
|
{
|
|
|
"command_id": command["command_id"],
|
|
"command_id": command["command_id"],
|
|
@@ -218,31 +408,126 @@ def _execute_pause(
|
|
|
now: datetime,
|
|
now: datetime,
|
|
|
start_hour: int,
|
|
start_hour: int,
|
|
|
client: TencentClient,
|
|
client: TencentClient,
|
|
|
-) -> tuple[int, int]:
|
|
|
|
|
|
|
+ items: list[dict[str, Any]],
|
|
|
|
|
+) -> tuple[int, int, int]:
|
|
|
account_id = int(account["account_id"])
|
|
account_id = int(account["account_id"])
|
|
|
- ads = client.get_ads(account_id)
|
|
|
|
|
|
|
+ try:
|
|
|
|
|
+ ads = {
|
|
|
|
|
+ int(ad.get("adgroup_id") or 0): ad
|
|
|
|
|
+ for ad in client.get_ads(account_id)
|
|
|
|
|
+ }
|
|
|
|
|
+ except Exception as exc:
|
|
|
|
|
+ for item in items:
|
|
|
|
|
+ _record_item(
|
|
|
|
|
+ command,
|
|
|
|
|
+ account,
|
|
|
|
|
+ item=item,
|
|
|
|
|
+ execution_status="FAILED",
|
|
|
|
|
+ error_message=f"Tencent ad query failed: {exc}",
|
|
|
|
|
+ )
|
|
|
|
|
+ return 0, len(items), 0
|
|
|
states = load_ad_states(account_id)
|
|
states = load_ad_states(account_id)
|
|
|
- successes = failures = 0
|
|
|
|
|
|
|
+ successes = failures = skipped = 0
|
|
|
mode = (
|
|
mode = (
|
|
|
PAUSE_UNTIL_NEXT_DELIVERY
|
|
PAUSE_UNTIL_NEXT_DELIVERY
|
|
|
if command["action"] == ACTION_DAY_PAUSE
|
|
if command["action"] == ACTION_DAY_PAUSE
|
|
|
else PAUSE_UNTIL_MANUAL
|
|
else PAUSE_UNTIL_MANUAL
|
|
|
)
|
|
)
|
|
|
resume_at = _next_delivery_start(now, start_hour) if mode == PAUSE_UNTIL_NEXT_DELIVERY else None
|
|
resume_at = _next_delivery_start(now, start_hour) if mode == PAUSE_UNTIL_NEXT_DELIVERY else None
|
|
|
- for ad in ads:
|
|
|
|
|
- adgroup_id = int(ad.get("adgroup_id") or 0)
|
|
|
|
|
|
|
+ for item in items:
|
|
|
|
|
+ adgroup_id = int(item["adgroup_id"])
|
|
|
|
|
+ ad = ads.get(adgroup_id)
|
|
|
|
|
+ if not ad:
|
|
|
|
|
+ _record_item(
|
|
|
|
|
+ command, account, item=item,
|
|
|
|
|
+ execution_status="FAILED", error_message="Tencent ad not found",
|
|
|
|
|
+ )
|
|
|
|
|
+ failures += 1
|
|
|
|
|
+ continue
|
|
|
before_status = str(ad.get("configured_status") or "")
|
|
before_status = str(ad.get("configured_status") or "")
|
|
|
if before_status != ACTIVE_STATUS:
|
|
if before_status != ACTIVE_STATUS:
|
|
|
|
|
+ existing_state = states.get(adgroup_id)
|
|
|
|
|
+ if (
|
|
|
|
|
+ command["action"] == ACTION_STOP
|
|
|
|
|
+ and before_status == SUSPEND_STATUS
|
|
|
|
|
+ and existing_state
|
|
|
|
|
+ and existing_state.get("operator_pause_mode")
|
|
|
|
|
+ ):
|
|
|
|
|
+ set_operator_pause(
|
|
|
|
|
+ account_id=account_id,
|
|
|
|
|
+ adgroup_id=adgroup_id,
|
|
|
|
|
+ mode=PAUSE_UNTIL_MANUAL,
|
|
|
|
|
+ resume_at=None,
|
|
|
|
|
+ command_id=command["command_id"],
|
|
|
|
|
+ paused_from_status=(
|
|
|
|
|
+ existing_state.get("operator_paused_from_status") or ACTIVE_STATUS
|
|
|
|
|
+ ),
|
|
|
|
|
+ paused_at=now,
|
|
|
|
|
+ )
|
|
|
|
|
+ _record_item(
|
|
|
|
|
+ command,
|
|
|
|
|
+ account,
|
|
|
|
|
+ item=item,
|
|
|
|
|
+ adgroup_id=adgroup_id,
|
|
|
|
|
+ adgroup_name=ad.get("adgroup_name"),
|
|
|
|
|
+ before_status=before_status,
|
|
|
|
|
+ target_status=SUSPEND_STATUS,
|
|
|
|
|
+ readback_status=before_status,
|
|
|
|
|
+ execution_status="SUCCESS",
|
|
|
|
|
+ )
|
|
|
|
|
+ successes += 1
|
|
|
|
|
+ continue
|
|
|
_record_item(
|
|
_record_item(
|
|
|
command,
|
|
command,
|
|
|
account,
|
|
account,
|
|
|
|
|
+ item=item,
|
|
|
adgroup_id=adgroup_id,
|
|
adgroup_id=adgroup_id,
|
|
|
adgroup_name=ad.get("adgroup_name"),
|
|
adgroup_name=ad.get("adgroup_name"),
|
|
|
before_status=before_status,
|
|
before_status=before_status,
|
|
|
- target_status=SUSPEND_STATUS,
|
|
|
|
|
|
|
+ target_status=(
|
|
|
|
|
+ before_status
|
|
|
|
|
+ if mode == PAUSE_UNTIL_NEXT_DELIVERY
|
|
|
|
|
+ else SUSPEND_STATUS
|
|
|
|
|
+ ),
|
|
|
readback_status=before_status,
|
|
readback_status=before_status,
|
|
|
execution_status="SKIPPED_NOT_ACTIVE",
|
|
execution_status="SKIPPED_NOT_ACTIVE",
|
|
|
)
|
|
)
|
|
|
|
|
+ skipped += 1
|
|
|
|
|
+ continue
|
|
|
|
|
+ if (
|
|
|
|
|
+ mode == PAUSE_UNTIL_NEXT_DELIVERY
|
|
|
|
|
+ and str(ad.get("begin_date") or "") > now.date().isoformat()
|
|
|
|
|
+ ):
|
|
|
|
|
+ _record_item(
|
|
|
|
|
+ command,
|
|
|
|
|
+ account,
|
|
|
|
|
+ item=item,
|
|
|
|
|
+ adgroup_id=adgroup_id,
|
|
|
|
|
+ adgroup_name=ad.get("adgroup_name"),
|
|
|
|
|
+ before_status=before_status,
|
|
|
|
|
+ target_status=before_status,
|
|
|
|
|
+ readback_status=before_status,
|
|
|
|
|
+ execution_status="SKIPPED_ALREADY_DEFERRED",
|
|
|
|
|
+ )
|
|
|
|
|
+ skipped += 1
|
|
|
|
|
+ continue
|
|
|
|
|
+ if (
|
|
|
|
|
+ mode == PAUSE_UNTIL_NEXT_DELIVERY
|
|
|
|
|
+ and str(ad.get("end_date") or "")
|
|
|
|
|
+ and str(ad.get("end_date")) < resume_at.date().isoformat()
|
|
|
|
|
+ ):
|
|
|
|
|
+ _record_item(
|
|
|
|
|
+ command,
|
|
|
|
|
+ account,
|
|
|
|
|
+ item=item,
|
|
|
|
|
+ adgroup_id=adgroup_id,
|
|
|
|
|
+ adgroup_name=ad.get("adgroup_name"),
|
|
|
|
|
+ before_status=before_status,
|
|
|
|
|
+ target_status=before_status,
|
|
|
|
|
+ readback_status=before_status,
|
|
|
|
|
+ execution_status="SKIPPED_END_DATE",
|
|
|
|
|
+ )
|
|
|
|
|
+ skipped += 1
|
|
|
continue
|
|
continue
|
|
|
tencent_updated = False
|
|
tencent_updated = False
|
|
|
try:
|
|
try:
|
|
@@ -256,20 +541,37 @@ def _execute_pause(
|
|
|
paused_from_status=before_status,
|
|
paused_from_status=before_status,
|
|
|
paused_at=now,
|
|
paused_at=now,
|
|
|
)
|
|
)
|
|
|
- readback = client.update_ad(
|
|
|
|
|
- account_id,
|
|
|
|
|
- adgroup_id,
|
|
|
|
|
- target_status=SUSPEND_STATUS,
|
|
|
|
|
- )
|
|
|
|
|
- tencent_updated = True
|
|
|
|
|
|
|
+ if mode == PAUSE_UNTIL_NEXT_DELIVERY:
|
|
|
|
|
+ before_begin_date = str(ad.get("begin_date") or "")
|
|
|
|
|
+ next_day = resume_at.date().isoformat()
|
|
|
|
|
+ target_begin_date = max(before_begin_date, next_day)
|
|
|
|
|
+ if before_begin_date != target_begin_date:
|
|
|
|
|
+ client.update_ad_begin_dates(
|
|
|
|
|
+ account_id,
|
|
|
|
|
+ [adgroup_id],
|
|
|
|
|
+ target_begin_date,
|
|
|
|
|
+ )
|
|
|
|
|
+ tencent_updated = True
|
|
|
|
|
+ readback_status = before_status
|
|
|
|
|
+ target_status = before_status
|
|
|
|
|
+ else:
|
|
|
|
|
+ readback = client.update_ad(
|
|
|
|
|
+ account_id,
|
|
|
|
|
+ adgroup_id,
|
|
|
|
|
+ target_status=SUSPEND_STATUS,
|
|
|
|
|
+ )
|
|
|
|
|
+ tencent_updated = True
|
|
|
|
|
+ readback_status = readback.get("configured_status")
|
|
|
|
|
+ target_status = SUSPEND_STATUS
|
|
|
_record_item(
|
|
_record_item(
|
|
|
command,
|
|
command,
|
|
|
account,
|
|
account,
|
|
|
|
|
+ item=item,
|
|
|
adgroup_id=adgroup_id,
|
|
adgroup_id=adgroup_id,
|
|
|
adgroup_name=ad.get("adgroup_name"),
|
|
adgroup_name=ad.get("adgroup_name"),
|
|
|
before_status=before_status,
|
|
before_status=before_status,
|
|
|
- target_status=SUSPEND_STATUS,
|
|
|
|
|
- readback_status=readback.get("configured_status"),
|
|
|
|
|
|
|
+ target_status=target_status,
|
|
|
|
|
+ readback_status=readback_status,
|
|
|
execution_status="SUCCESS",
|
|
execution_status="SUCCESS",
|
|
|
)
|
|
)
|
|
|
successes += 1
|
|
successes += 1
|
|
@@ -294,14 +596,23 @@ def _execute_pause(
|
|
|
_record_item(
|
|
_record_item(
|
|
|
command,
|
|
command,
|
|
|
account,
|
|
account,
|
|
|
|
|
+ item=item,
|
|
|
adgroup_id=adgroup_id,
|
|
adgroup_id=adgroup_id,
|
|
|
adgroup_name=ad.get("adgroup_name"),
|
|
adgroup_name=ad.get("adgroup_name"),
|
|
|
before_status=before_status,
|
|
before_status=before_status,
|
|
|
- target_status=SUSPEND_STATUS,
|
|
|
|
|
|
|
+ target_status=(
|
|
|
|
|
+ before_status
|
|
|
|
|
+ if mode == PAUSE_UNTIL_NEXT_DELIVERY
|
|
|
|
|
+ else SUSPEND_STATUS
|
|
|
|
|
+ ),
|
|
|
readback_status=(
|
|
readback_status=(
|
|
|
exc.actual.get("configured_status")
|
|
exc.actual.get("configured_status")
|
|
|
if isinstance(exc, PostWriteVerificationError)
|
|
if isinstance(exc, PostWriteVerificationError)
|
|
|
- else SUSPEND_STATUS
|
|
|
|
|
|
|
+ else (
|
|
|
|
|
+ before_status
|
|
|
|
|
+ if mode == PAUSE_UNTIL_NEXT_DELIVERY
|
|
|
|
|
+ else SUSPEND_STATUS
|
|
|
|
|
+ )
|
|
|
if tencent_updated
|
|
if tencent_updated
|
|
|
else None
|
|
else None
|
|
|
),
|
|
),
|
|
@@ -325,7 +636,7 @@ def _execute_pause(
|
|
|
adgroup_id,
|
|
adgroup_id,
|
|
|
)
|
|
)
|
|
|
failures += 1
|
|
failures += 1
|
|
|
- return successes, failures
|
|
|
|
|
|
|
+ return successes, failures, skipped
|
|
|
|
|
|
|
|
|
|
|
|
|
def _execute_resume(
|
|
def _execute_resume(
|
|
@@ -334,23 +645,45 @@ def _execute_resume(
|
|
|
*,
|
|
*,
|
|
|
now: datetime,
|
|
now: datetime,
|
|
|
client: TencentClient,
|
|
client: TencentClient,
|
|
|
-) -> tuple[int, int]:
|
|
|
|
|
|
|
+ items: list[dict[str, Any]],
|
|
|
|
|
+) -> tuple[int, int, int]:
|
|
|
account_id = int(account["account_id"])
|
|
account_id = int(account["account_id"])
|
|
|
- pauses = load_operator_pauses([account_id])
|
|
|
|
|
- if not pauses:
|
|
|
|
|
- return 0, 0
|
|
|
|
|
- ads = {
|
|
|
|
|
- int(ad.get("adgroup_id") or 0): ad
|
|
|
|
|
- for ad in client.get_ads(account_id)
|
|
|
|
|
|
|
+ pauses = {
|
|
|
|
|
+ int(row["adgroup_id"]): row
|
|
|
|
|
+ for row in load_operator_pauses([account_id])
|
|
|
}
|
|
}
|
|
|
- successes = failures = 0
|
|
|
|
|
- for state in pauses:
|
|
|
|
|
- adgroup_id = int(state["adgroup_id"])
|
|
|
|
|
|
|
+ try:
|
|
|
|
|
+ ads = {
|
|
|
|
|
+ int(ad.get("adgroup_id") or 0): ad
|
|
|
|
|
+ for ad in client.get_ads(account_id)
|
|
|
|
|
+ }
|
|
|
|
|
+ except Exception as exc:
|
|
|
|
|
+ for item in items:
|
|
|
|
|
+ _record_item(
|
|
|
|
|
+ command,
|
|
|
|
|
+ account,
|
|
|
|
|
+ item=item,
|
|
|
|
|
+ execution_status="FAILED",
|
|
|
|
|
+ error_message=f"Tencent ad query failed: {exc}",
|
|
|
|
|
+ )
|
|
|
|
|
+ return 0, len(items), 0
|
|
|
|
|
+ successes = failures = skipped = 0
|
|
|
|
|
+ for item in items:
|
|
|
|
|
+ adgroup_id = int(item["adgroup_id"])
|
|
|
|
|
+ state = pauses.get(adgroup_id)
|
|
|
|
|
+ if not state:
|
|
|
|
|
+ _record_item(
|
|
|
|
|
+ command, account, item=item,
|
|
|
|
|
+ execution_status="SKIPPED_NOT_PAUSED",
|
|
|
|
|
+ )
|
|
|
|
|
+ skipped += 1
|
|
|
|
|
+ continue
|
|
|
ad = ads.get(adgroup_id)
|
|
ad = ads.get(adgroup_id)
|
|
|
if not ad:
|
|
if not ad:
|
|
|
_record_item(
|
|
_record_item(
|
|
|
command,
|
|
command,
|
|
|
account,
|
|
account,
|
|
|
|
|
+ item=item,
|
|
|
adgroup_id=adgroup_id,
|
|
adgroup_id=adgroup_id,
|
|
|
adgroup_name=state.get("adgroup_name"),
|
|
adgroup_name=state.get("adgroup_name"),
|
|
|
execution_status="FAILED",
|
|
execution_status="FAILED",
|
|
@@ -362,7 +695,20 @@ def _execute_resume(
|
|
|
try:
|
|
try:
|
|
|
readback_status = before_status
|
|
readback_status = before_status
|
|
|
target_status = None
|
|
target_status = None
|
|
|
- if before_status == SUSPEND_STATUS and not bool(state.get("paused_by_strategy")):
|
|
|
|
|
|
|
+ if (
|
|
|
|
|
+ state.get("operator_pause_mode") == PAUSE_UNTIL_NEXT_DELIVERY
|
|
|
|
|
+ and str(ad.get("begin_date") or "") > now.date().isoformat()
|
|
|
|
|
+ and not bool(state.get("paused_by_strategy"))
|
|
|
|
|
+ ):
|
|
|
|
|
+ client.update_ad_begin_dates(
|
|
|
|
|
+ account_id,
|
|
|
|
|
+ [adgroup_id],
|
|
|
|
|
+ now.date().isoformat(),
|
|
|
|
|
+ )
|
|
|
|
|
+ elif (
|
|
|
|
|
+ before_status == SUSPEND_STATUS
|
|
|
|
|
+ and not bool(state.get("paused_by_strategy"))
|
|
|
|
|
+ ):
|
|
|
target_status = ACTIVE_STATUS
|
|
target_status = ACTIVE_STATUS
|
|
|
readback = client.update_ad(
|
|
readback = client.update_ad(
|
|
|
account_id,
|
|
account_id,
|
|
@@ -379,6 +725,7 @@ def _execute_resume(
|
|
|
_record_item(
|
|
_record_item(
|
|
|
command,
|
|
command,
|
|
|
account,
|
|
account,
|
|
|
|
|
+ item=item,
|
|
|
adgroup_id=adgroup_id,
|
|
adgroup_id=adgroup_id,
|
|
|
adgroup_name=ad.get("adgroup_name"),
|
|
adgroup_name=ad.get("adgroup_name"),
|
|
|
before_status=before_status,
|
|
before_status=before_status,
|
|
@@ -391,6 +738,7 @@ def _execute_resume(
|
|
|
_record_item(
|
|
_record_item(
|
|
|
command,
|
|
command,
|
|
|
account,
|
|
account,
|
|
|
|
|
+ item=item,
|
|
|
adgroup_id=adgroup_id,
|
|
adgroup_id=adgroup_id,
|
|
|
adgroup_name=ad.get("adgroup_name"),
|
|
adgroup_name=ad.get("adgroup_name"),
|
|
|
before_status=before_status,
|
|
before_status=before_status,
|
|
@@ -399,7 +747,7 @@ def _execute_resume(
|
|
|
error_message=str(exc),
|
|
error_message=str(exc),
|
|
|
)
|
|
)
|
|
|
failures += 1
|
|
failures += 1
|
|
|
- return successes, failures
|
|
|
|
|
|
|
+ return successes, failures, skipped
|
|
|
|
|
|
|
|
|
|
|
|
|
def execute_confirmed_command(
|
|
def execute_confirmed_command(
|
|
@@ -470,27 +818,36 @@ def execute_confirmed_command(
|
|
|
for account_id in command["target_account_ids"]
|
|
for account_id in command["target_account_ids"]
|
|
|
]
|
|
]
|
|
|
client = tencent or TencentClient()
|
|
client = tencent or TencentClient()
|
|
|
- successes = failures = 0
|
|
|
|
|
|
|
+ frozen_items = load_operator_command_items(command_id)
|
|
|
|
|
+ if not frozen_items:
|
|
|
|
|
+ raise RuntimeError("命令缺少冻结广告明细,请重新发起")
|
|
|
|
|
+ items_by_account: dict[int, list[dict[str, Any]]] = {}
|
|
|
|
|
+ for item in frozen_items:
|
|
|
|
|
+ items_by_account.setdefault(int(item["account_id"]), []).append(item)
|
|
|
|
|
+ successes = failures = skipped = 0
|
|
|
for account in accounts:
|
|
for account in accounts:
|
|
|
if command["action"] in {ACTION_DAY_PAUSE, ACTION_STOP}:
|
|
if command["action"] in {ACTION_DAY_PAUSE, ACTION_STOP}:
|
|
|
- ok, failed = _execute_pause(
|
|
|
|
|
|
|
+ ok, failed, ignored = _execute_pause(
|
|
|
command,
|
|
command,
|
|
|
account,
|
|
account,
|
|
|
now=now,
|
|
now=now,
|
|
|
start_hour=start_hour,
|
|
start_hour=start_hour,
|
|
|
client=client,
|
|
client=client,
|
|
|
|
|
+ items=items_by_account.get(int(account["account_id"]), []),
|
|
|
)
|
|
)
|
|
|
elif command["action"] == ACTION_RESUME:
|
|
elif command["action"] == ACTION_RESUME:
|
|
|
- ok, failed = _execute_resume(
|
|
|
|
|
|
|
+ ok, failed, ignored = _execute_resume(
|
|
|
command,
|
|
command,
|
|
|
account,
|
|
account,
|
|
|
now=now,
|
|
now=now,
|
|
|
client=client,
|
|
client=client,
|
|
|
|
|
+ items=items_by_account.get(int(account["account_id"]), []),
|
|
|
)
|
|
)
|
|
|
else:
|
|
else:
|
|
|
raise ValueError(f"Unsupported command action: {command['action']}")
|
|
raise ValueError(f"Unsupported command action: {command['action']}")
|
|
|
successes += ok
|
|
successes += ok
|
|
|
failures += failed
|
|
failures += failed
|
|
|
|
|
+ skipped += ignored
|
|
|
except Exception as exc:
|
|
except Exception as exc:
|
|
|
update_operator_command(
|
|
update_operator_command(
|
|
|
command_id,
|
|
command_id,
|
|
@@ -511,12 +868,16 @@ def execute_confirmed_command(
|
|
|
result = load_operator_command(command_id) or command
|
|
result = load_operator_command(command_id) or command
|
|
|
result["successes"] = successes
|
|
result["successes"] = successes
|
|
|
result["failures"] = failures
|
|
result["failures"] = failures
|
|
|
|
|
+ result["skipped"] = skipped
|
|
|
return result
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
-def pause_status_summary() -> dict[str, Any]:
|
|
|
|
|
- account_ids = [int(row["account_id"]) for row in load_managed_accounts()]
|
|
|
|
|
- pauses = load_operator_pauses(account_ids)
|
|
|
|
|
|
|
+def pause_status_summary(now: datetime) -> dict[str, Any]:
|
|
|
|
|
+ account_ids = [int(row["account_id"]) for row in load_realtime_accounts()]
|
|
|
|
|
+ pauses = [
|
|
|
|
|
+ row for row in load_operator_pauses(account_ids)
|
|
|
|
|
+ if _operator_pause_is_active(row, now)
|
|
|
|
|
+ ]
|
|
|
return {
|
|
return {
|
|
|
"total": len(pauses),
|
|
"total": len(pauses),
|
|
|
"until_next_delivery": sum(
|
|
"until_next_delivery": sum(
|