| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170 |
- """MySQL state and audit storage for real-time control."""
- from __future__ import annotations
- import os
- import json
- from contextlib import contextmanager
- from datetime import date, datetime
- from pathlib import Path
- from typing import Any, Iterator
- import pymysql
- import pymysql.cursors
- ROOT = Path(__file__).resolve().parent
- def connect() -> pymysql.Connection:
- required = ["DB_HOST", "DB_USER", "DB_NAME"]
- missing = [key for key in required if not os.getenv(key)]
- if missing:
- raise RuntimeError(f"Missing database environment variables: {', '.join(missing)}")
- return pymysql.connect(
- host=os.environ["DB_HOST"],
- port=int(os.getenv("DB_PORT", "3306")),
- user=os.environ["DB_USER"],
- password=os.getenv("DB_PASSWORD", ""),
- database=os.environ["DB_NAME"],
- charset="utf8mb4",
- cursorclass=pymysql.cursors.DictCursor,
- autocommit=True,
- connect_timeout=int(os.getenv("DB_CONNECT_TIMEOUT", "10")),
- read_timeout=int(os.getenv("DB_READ_TIMEOUT", "60")),
- write_timeout=int(os.getenv("DB_WRITE_TIMEOUT", "60")),
- )
- def initialize_schema() -> None:
- statements = [
- statement.strip()
- for statement in (ROOT / "schema.sql").read_text(encoding="utf-8").split(";")
- if statement.strip()
- ]
- connection = connect()
- try:
- with connection.cursor() as cursor:
- for statement in statements:
- cursor.execute(statement)
- cursor.execute(
- """
- SELECT COLUMN_NAME
- FROM information_schema.COLUMNS
- WHERE TABLE_SCHEMA=%s
- AND TABLE_NAME='realtime_control_ad_state'
- """,
- (os.environ["DB_NAME"],),
- )
- existing_columns = {row["COLUMN_NAME"] for row in cursor.fetchall()}
- migrations = {
- "initial_base_bid_fen": "INT DEFAULT NULL",
- "last_roi_scaled_at": "DATETIME DEFAULT NULL",
- "bid_hold": "BOOLEAN NOT NULL DEFAULT FALSE",
- "bid_hold_reason": "VARCHAR(255) DEFAULT NULL",
- "operator_pause_mode": "VARCHAR(32) DEFAULT NULL",
- "operator_resume_at": "DATETIME DEFAULT NULL",
- "operator_command_id": "VARCHAR(64) DEFAULT NULL",
- "operator_paused_from_status": "VARCHAR(50) DEFAULT NULL",
- "operator_paused_at": "DATETIME DEFAULT NULL",
- }
- for column, definition in migrations.items():
- if column not in existing_columns:
- cursor.execute(
- f"ALTER TABLE realtime_control_ad_state "
- f"ADD COLUMN {column} {definition}"
- )
- cursor.execute(
- """
- UPDATE realtime_control_ad_state
- SET initial_base_bid_fen=base_bid_fen
- WHERE initial_base_bid_fen IS NULL
- """
- )
- cursor.execute(
- """
- SELECT INDEX_NAME
- FROM information_schema.STATISTICS
- WHERE TABLE_SCHEMA=%s
- AND TABLE_NAME='realtime_control_ad_state'
- AND INDEX_NAME='idx_operator_pause'
- """,
- (os.environ["DB_NAME"],),
- )
- if not cursor.fetchone():
- cursor.execute(
- """
- CREATE INDEX idx_operator_pause
- ON realtime_control_ad_state
- (operator_pause_mode, operator_resume_at)
- """
- )
- operator_migrations = {
- "operator_command": {
- "raw_text": "MEDIUMTEXT DEFAULT NULL",
- "parse_source": "VARCHAR(32) NOT NULL DEFAULT 'deterministic'",
- "intent_json": "MEDIUMTEXT DEFAULT NULL",
- "preview_cost_fen": "BIGINT NOT NULL DEFAULT 0",
- "preview_impressions": "BIGINT NOT NULL DEFAULT 0",
- "preview_clicks": "BIGINT NOT NULL DEFAULT 0",
- "preview_conversions": "BIGINT NOT NULL DEFAULT 0",
- "previewed_at": "DATETIME DEFAULT NULL",
- "resume_at": "DATETIME DEFAULT NULL",
- },
- "operator_command_item": {
- "preview_cost_fen": "BIGINT NOT NULL DEFAULT 0",
- "preview_impressions": "BIGINT NOT NULL DEFAULT 0",
- "preview_clicks": "BIGINT NOT NULL DEFAULT 0",
- "preview_conversions": "BIGINT NOT NULL DEFAULT 0",
- "previewed_at": "DATETIME DEFAULT NULL",
- "updated_at": (
- "TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP "
- "ON UPDATE CURRENT_TIMESTAMP"
- ),
- },
- }
- for table_name, columns in operator_migrations.items():
- cursor.execute(
- """
- SELECT COLUMN_NAME
- FROM information_schema.COLUMNS
- WHERE TABLE_SCHEMA=%s AND TABLE_NAME=%s
- """,
- (os.environ["DB_NAME"], table_name),
- )
- table_columns = {row["COLUMN_NAME"] for row in cursor.fetchall()}
- for column, definition in columns.items():
- if column not in table_columns:
- cursor.execute(
- f"ALTER TABLE {table_name} ADD COLUMN {column} {definition}"
- )
- roi_migrations = {
- "roi_metric_run": {
- "fission_parameter_version": "VARCHAR(64) DEFAULT NULL",
- "fission_cohort_date": "DATE DEFAULT NULL",
- },
- "roi_entity_snapshot": {
- "conversion_goal": "VARCHAR(255) DEFAULT NULL",
- "fission_parameter_version": "VARCHAR(64) DEFAULT NULL",
- "fission_cohort_date": "VARCHAR(8) DEFAULT NULL",
- "fission_multiplier_vs_t0": "DECIMAL(18,8) DEFAULT NULL",
- "fission_multiplier_vs_first": "DECIMAL(18,8) DEFAULT NULL",
- "fission_match_level": "VARCHAR(64) DEFAULT NULL",
- "fission_source": "VARCHAR(512) DEFAULT NULL",
- "actual_total_revenue": "DECIMAL(20,4) DEFAULT NULL",
- "actual_roi": "DECIMAL(18,8) DEFAULT NULL",
- "predicted_tail_revenue": "DECIMAL(20,4) DEFAULT NULL",
- "predicted_fission_revenue": "DECIMAL(20,4) DEFAULT NULL",
- },
- "roi_fission_parameter_value": {
- "multiplier_vs_first": "DOUBLE DEFAULT NULL",
- },
- "roi_action_item": {
- "approval_status": "VARCHAR(32) NOT NULL DEFAULT 'PENDING'",
- "approval_source": "VARCHAR(32) DEFAULT NULL",
- "approved_at": "DATETIME DEFAULT NULL",
- "rejected_at": "DATETIME DEFAULT NULL",
- "sheet_row_number": "INT DEFAULT NULL",
- "result_notified_at": "DATETIME DEFAULT NULL",
- "notification_error": "TEXT DEFAULT NULL",
- },
- }
- for table_name, columns in roi_migrations.items():
- cursor.execute(
- """
- SELECT COLUMN_NAME
- FROM information_schema.COLUMNS
- WHERE TABLE_SCHEMA=%s AND TABLE_NAME=%s
- """,
- (os.environ["DB_NAME"], table_name),
- )
- table_columns = {
- row["COLUMN_NAME"] for row in cursor.fetchall()
- }
- for column, definition in columns.items():
- if column not in table_columns:
- cursor.execute(
- f"ALTER TABLE {table_name} "
- f"ADD COLUMN {column} {definition}"
- )
- cursor.execute(
- """
- SELECT INDEX_NAME
- FROM information_schema.STATISTICS
- WHERE TABLE_SCHEMA=%s
- AND TABLE_NAME='roi_action_item'
- AND INDEX_NAME='idx_roi_action_approval'
- """,
- (os.environ["DB_NAME"],),
- )
- if not cursor.fetchone():
- cursor.execute(
- """
- CREATE INDEX idx_roi_action_approval
- ON roi_action_item (run_id, approval_status)
- """
- )
- finally:
- connection.close()
- @contextmanager
- def advisory_lock(lock_name: str) -> Iterator[bool]:
- connection = connect()
- acquired = False
- try:
- with connection.cursor() as cursor:
- cursor.execute("SELECT GET_LOCK(%s, 0) AS acquired", (lock_name,))
- acquired = bool((cursor.fetchone() or {}).get("acquired"))
- yield acquired
- finally:
- if acquired:
- try:
- with connection.cursor() as cursor:
- cursor.execute("SELECT RELEASE_LOCK(%s)", (lock_name,))
- except Exception:
- pass
- connection.close()
- def load_enabled_accounts() -> list[dict[str, Any]]:
- connection = connect()
- try:
- with connection.cursor() as cursor:
- cursor.execute(
- """
- SELECT c.account_id, c.audience_name, c.bid_scene
- FROM ad_creation_account_config c
- JOIN account_whitelist w ON w.account_id = c.account_id
- WHERE c.enabled = TRUE
- AND w.enabled = TRUE
- ORDER BY c.account_id
- """
- )
- return list(cursor.fetchall())
- finally:
- connection.close()
- def load_automation_spend_accounts() -> list[dict[str, Any]]:
- """Load historical automation accounts that remain whitelisted."""
- connection = connect()
- try:
- with connection.cursor() as cursor:
- cursor.execute(
- """
- SELECT c.account_id
- FROM ad_creation_account_config c
- JOIN account_whitelist w ON w.account_id = c.account_id
- WHERE w.enabled = TRUE
- ORDER BY c.account_id
- """
- )
- return list(cursor.fetchall())
- finally:
- connection.close()
- def load_all_spend_accounts() -> list[dict[str, Any]]:
- """Load every enabled account from the local account whitelist."""
- connection = connect()
- try:
- with connection.cursor() as cursor:
- cursor.execute(
- """
- SELECT w.account_id
- FROM account_whitelist w
- WHERE w.enabled = TRUE
- ORDER BY w.account_id
- """
- )
- return list(cursor.fetchall())
- finally:
- connection.close()
- def load_realtime_accounts() -> list[dict[str, Any]]:
- """Load automation accounts plus explicit real-time scope overrides."""
- connection = connect()
- try:
- with connection.cursor() as cursor:
- cursor.execute(
- """
- SELECT c.account_id, c.audience_name, c.bid_scene,
- 'FULL' AS control_mode
- FROM ad_creation_account_config c
- JOIN account_whitelist w ON w.account_id = c.account_id
- WHERE w.enabled = TRUE
- ORDER BY c.account_id
- """
- )
- accounts = {
- int(row["account_id"]): row for row in cursor.fetchall()
- }
- cursor.execute(
- """
- SELECT account_id, audience_name, bid_scene, control_mode
- FROM realtime_control_account_scope
- WHERE enabled=TRUE
- ORDER BY account_id
- """
- )
- for row in cursor.fetchall():
- accounts[int(row["account_id"])] = row
- return [accounts[key] for key in sorted(accounts)]
- finally:
- connection.close()
- def upsert_realtime_account_scope(
- *,
- account_id: int,
- control_mode: str,
- audience_name: str,
- bid_scene: str | None,
- source: str,
- note: str,
- ) -> None:
- if control_mode not in {"FULL", "PAUSE_ONLY"}:
- raise ValueError(f"Unsupported real-time control mode: {control_mode}")
- connection = connect()
- try:
- with connection.cursor() as cursor:
- cursor.execute(
- """
- INSERT INTO realtime_control_account_scope
- (account_id, control_mode, audience_name, bid_scene,
- enabled, source, note)
- VALUES (%s,%s,%s,%s,TRUE,%s,%s)
- ON DUPLICATE KEY UPDATE
- control_mode=VALUES(control_mode),
- audience_name=VALUES(audience_name),
- bid_scene=VALUES(bid_scene),
- enabled=TRUE,
- source=VALUES(source),
- note=VALUES(note)
- """,
- (
- account_id,
- control_mode,
- audience_name,
- bid_scene,
- source,
- note,
- ),
- )
- finally:
- connection.close()
- def load_managed_accounts() -> list[dict[str, Any]]:
- """Historical automation accounts still allowed by the account whitelist."""
- connection = connect()
- try:
- with connection.cursor() as cursor:
- cursor.execute(
- """
- SELECT c.account_id, c.audience_name, c.bid_scene, c.enabled
- FROM ad_creation_account_config c
- JOIN account_whitelist w ON w.account_id = c.account_id
- WHERE w.enabled = TRUE
- ORDER BY c.account_id
- """
- )
- return list(cursor.fetchall())
- finally:
- connection.close()
- def load_daily_state(control_date: date) -> dict[str, Any]:
- connection = connect()
- try:
- with connection.cursor() as cursor:
- cursor.execute(
- "SELECT * FROM realtime_control_daily_state WHERE control_date=%s",
- (control_date,),
- )
- return cursor.fetchone() or {}
- finally:
- connection.close()
- def save_daily_state(control_date: date, **values: Any) -> None:
- allowed = {
- "morning_recovery_done",
- "cutoff_done",
- "last_observed_partition",
- "last_observed_cpm",
- "last_decision",
- "last_inventory_refresh_at",
- "last_evaluated_at",
- }
- unknown = set(values) - allowed
- if unknown:
- raise ValueError(f"Unsupported daily-state fields: {sorted(unknown)}")
- columns = ["control_date", *values]
- params = [control_date, *values.values()]
- updates = ", ".join(f"{column}=VALUES({column})" for column in values)
- placeholders = ", ".join(["%s"] * len(columns))
- sql = (
- f"INSERT INTO realtime_control_daily_state ({', '.join(columns)}) "
- f"VALUES ({placeholders}) ON DUPLICATE KEY UPDATE {updates}"
- )
- connection = connect()
- try:
- with connection.cursor() as cursor:
- cursor.execute(sql, params)
- finally:
- connection.close()
- def load_ad_states(account_id: int) -> dict[int, dict[str, Any]]:
- connection = connect()
- try:
- with connection.cursor() as cursor:
- cursor.execute(
- "SELECT * FROM realtime_control_ad_state WHERE account_id=%s",
- (account_id,),
- )
- return {int(row["adgroup_id"]): row for row in cursor.fetchall()}
- finally:
- connection.close()
- def upsert_ad_state(
- *,
- account_id: int,
- adgroup_id: int,
- adgroup_name: str,
- bid_field: str,
- base_bid_fen: int,
- boosted_date: date | None,
- paused_by_strategy: bool,
- pause_reason: str | None,
- last_action: str,
- action_at: datetime,
- ) -> None:
- connection = connect()
- try:
- with connection.cursor() as cursor:
- cursor.execute(
- """
- INSERT INTO realtime_control_ad_state
- (account_id, adgroup_id, adgroup_name, bid_field, base_bid_fen,
- initial_base_bid_fen,
- boosted_date, paused_by_strategy, pause_reason, last_action,
- last_action_at, last_seen_at)
- VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
- ON DUPLICATE KEY UPDATE
- adgroup_name=VALUES(adgroup_name),
- bid_field=VALUES(bid_field),
- initial_base_bid_fen=COALESCE(
- initial_base_bid_fen,
- VALUES(initial_base_bid_fen)
- ),
- boosted_date=VALUES(boosted_date),
- paused_by_strategy=VALUES(paused_by_strategy),
- pause_reason=VALUES(pause_reason),
- last_action=VALUES(last_action),
- last_action_at=VALUES(last_action_at),
- last_seen_at=VALUES(last_seen_at)
- """,
- (
- account_id,
- adgroup_id,
- adgroup_name,
- bid_field,
- base_bid_fen,
- base_bid_fen,
- boosted_date,
- paused_by_strategy,
- pause_reason,
- last_action,
- action_at,
- action_at,
- ),
- )
- finally:
- connection.close()
- def sync_roi_base_bid(
- *,
- account_id: int,
- adgroup_id: int,
- adgroup_name: str,
- bid_field: str,
- initial_base_bid_fen: int,
- new_base_bid_fen: int,
- boosted_date: date | None,
- action_at: datetime,
- ) -> None:
- """Persist a permanent ROI base bid without losing intraday boost state."""
- connection = connect()
- try:
- with connection.cursor() as cursor:
- cursor.execute(
- """
- INSERT INTO realtime_control_ad_state
- (account_id, adgroup_id, adgroup_name, bid_field,
- base_bid_fen, initial_base_bid_fen, boosted_date,
- paused_by_strategy, pause_reason, last_action,
- last_action_at, last_seen_at, last_roi_scaled_at)
- VALUES (%s,%s,%s,%s,%s,%s,%s,FALSE,NULL,'ROI_SCALE',%s,%s,%s)
- ON DUPLICATE KEY UPDATE
- adgroup_name=VALUES(adgroup_name),
- bid_field=VALUES(bid_field),
- base_bid_fen=VALUES(base_bid_fen),
- initial_base_bid_fen=COALESCE(
- initial_base_bid_fen,
- VALUES(initial_base_bid_fen)
- ),
- boosted_date=VALUES(boosted_date),
- last_action='ROI_SCALE',
- last_action_at=VALUES(last_action_at),
- last_seen_at=VALUES(last_seen_at),
- last_roi_scaled_at=VALUES(last_roi_scaled_at)
- """,
- (
- account_id,
- adgroup_id,
- adgroup_name,
- bid_field,
- new_base_bid_fen,
- initial_base_bid_fen,
- boosted_date,
- action_at,
- action_at,
- action_at,
- ),
- )
- finally:
- connection.close()
- def set_bid_experiment_hold(
- *,
- account_id: int,
- adgroup_id: int,
- adgroup_name: str,
- bid_field: str,
- base_bid_fen: int,
- action_at: datetime,
- reason: str,
- ) -> None:
- """Persist an experiment base bid and prevent real-time bid overrides."""
- connection = connect()
- try:
- with connection.cursor() as cursor:
- cursor.execute(
- """
- INSERT INTO realtime_control_ad_state
- (account_id, adgroup_id, adgroup_name, bid_field,
- base_bid_fen, initial_base_bid_fen, boosted_date,
- bid_hold, bid_hold_reason, paused_by_strategy,
- last_action, last_action_at, last_seen_at)
- VALUES (%s,%s,%s,%s,%s,%s,%s,TRUE,%s,FALSE,
- 'BID_EXPERIMENT_HOLD',%s,%s)
- ON DUPLICATE KEY UPDATE
- adgroup_name=VALUES(adgroup_name),
- bid_field=VALUES(bid_field),
- base_bid_fen=VALUES(base_bid_fen),
- initial_base_bid_fen=COALESCE(
- initial_base_bid_fen,
- VALUES(initial_base_bid_fen)
- ),
- boosted_date=VALUES(boosted_date),
- bid_hold=TRUE,
- bid_hold_reason=VALUES(bid_hold_reason),
- last_action='BID_EXPERIMENT_HOLD',
- last_action_at=VALUES(last_action_at),
- last_seen_at=VALUES(last_seen_at)
- """,
- (
- account_id,
- adgroup_id,
- adgroup_name,
- bid_field,
- base_bid_fen,
- base_bid_fen,
- action_at.date(),
- reason,
- action_at,
- action_at,
- ),
- )
- finally:
- connection.close()
- def clear_bid_experiment_hold(
- account_id: int,
- adgroup_id: int,
- *,
- action_at: datetime,
- ) -> None:
- connection = connect()
- try:
- with connection.cursor() as cursor:
- cursor.execute(
- """
- UPDATE realtime_control_ad_state
- SET bid_hold=FALSE,
- bid_hold_reason=NULL,
- last_action='BID_EXPERIMENT_RELEASE',
- last_action_at=%s,
- last_seen_at=%s
- WHERE account_id=%s AND adgroup_id=%s
- """,
- (action_at, action_at, account_id, adgroup_id),
- )
- if cursor.rowcount != 1:
- raise ValueError(
- "未找到实验出价状态: "
- f"account={account_id} adgroup={adgroup_id}"
- )
- finally:
- connection.close()
- def insert_action_log(record: dict[str, Any]) -> None:
- columns = [
- "run_id",
- "control_date",
- "observed_partition",
- "observed_cpm",
- "decision",
- "account_id",
- "adgroup_id",
- "adgroup_name",
- "bid_field",
- "base_bid_fen",
- "before_bid_fen",
- "target_bid_fen",
- "before_status",
- "target_status",
- "apply_mode",
- "execution_status",
- "error_message",
- ]
- connection = connect()
- try:
- with connection.cursor() as cursor:
- cursor.execute(
- f"INSERT INTO realtime_control_action_log ({', '.join(columns)}) "
- f"VALUES ({', '.join(['%s'] * len(columns))})",
- [record.get(column) for column in columns],
- )
- finally:
- connection.close()
- def create_operator_command(record: dict[str, Any]) -> dict[str, Any]:
- connection = connect()
- try:
- with connection.cursor() as cursor:
- try:
- cursor.execute(
- """
- INSERT INTO operator_command
- (command_id, source_message_id, chat_id, sender_open_id,
- sender_name, action, scope_type, target_account_ids,
- status, preview_account_count, preview_ad_count, expires_at)
- VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
- """,
- (
- record["command_id"],
- record["source_message_id"],
- record["chat_id"],
- record["sender_open_id"],
- record.get("sender_name"),
- record["action"],
- record["scope_type"],
- json.dumps(record["target_account_ids"]),
- record["status"],
- record.get("preview_account_count", 0),
- record.get("preview_ad_count", 0),
- record.get("expires_at"),
- ),
- )
- except pymysql.err.IntegrityError:
- cursor.execute(
- "SELECT * FROM operator_command WHERE source_message_id=%s",
- (record["source_message_id"],),
- )
- existing = cursor.fetchone()
- if existing:
- existing["target_account_ids"] = json.loads(
- existing.get("target_account_ids") or "[]"
- )
- return existing
- raise
- return load_operator_command(record["command_id"]) or {}
- finally:
- connection.close()
- def create_operator_command_with_items(
- record: dict[str, Any],
- items: list[dict[str, Any]],
- ) -> dict[str, Any]:
- """Atomically persist an immutable command preview and its ad snapshots."""
- connection = connect()
- try:
- connection.begin()
- with connection.cursor() as cursor:
- try:
- cursor.execute(
- """
- INSERT INTO operator_command
- (command_id, source_message_id, chat_id, sender_open_id,
- sender_name, raw_text, parse_source, intent_json,
- action, scope_type, target_account_ids, status,
- preview_account_count, preview_ad_count,
- preview_cost_fen, preview_impressions, preview_clicks,
- preview_conversions, previewed_at, resume_at, expires_at)
- VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
- """,
- (
- record["command_id"], record["source_message_id"],
- record["chat_id"], record["sender_open_id"],
- record.get("sender_name"), record.get("raw_text"),
- record.get("parse_source", "deterministic"),
- json.dumps(record.get("intent") or {}, ensure_ascii=False),
- record["action"], record["scope_type"],
- json.dumps(record["target_account_ids"]), record["status"],
- record.get("preview_account_count", 0),
- record.get("preview_ad_count", 0),
- record.get("preview_cost_fen", 0),
- record.get("preview_impressions", 0),
- record.get("preview_clicks", 0),
- record.get("preview_conversions", 0),
- record.get("previewed_at"), record.get("resume_at"),
- record.get("expires_at"),
- ),
- )
- except pymysql.err.IntegrityError:
- connection.rollback()
- existing = load_operator_command_by_source(record["source_message_id"])
- if existing:
- return existing
- raise
- columns = [
- "command_id", "account_id", "audience_name", "adgroup_id",
- "adgroup_name", "before_status", "target_status",
- "preview_cost_fen", "preview_impressions", "preview_clicks",
- "preview_conversions", "previewed_at", "execution_status",
- ]
- for item in items:
- cursor.execute(
- f"INSERT INTO operator_command_item ({', '.join(columns)}) "
- f"VALUES ({', '.join(['%s'] * len(columns))})",
- [item.get(column) for column in columns],
- )
- connection.commit()
- return load_operator_command(record["command_id"]) or {}
- except Exception:
- connection.rollback()
- raise
- finally:
- connection.close()
- def load_operator_command_by_source(source_message_id: str) -> dict[str, Any] | None:
- connection = connect()
- try:
- with connection.cursor() as cursor:
- cursor.execute(
- "SELECT command_id FROM operator_command WHERE source_message_id=%s",
- (source_message_id,),
- )
- row = cursor.fetchone()
- return load_operator_command(row["command_id"]) if row else None
- finally:
- connection.close()
- def load_operator_command(command_id: str) -> dict[str, Any] | None:
- connection = connect()
- try:
- with connection.cursor() as cursor:
- cursor.execute(
- "SELECT * FROM operator_command WHERE command_id=%s",
- (command_id,),
- )
- row = cursor.fetchone()
- if row:
- row["target_account_ids"] = json.loads(
- row.get("target_account_ids") or "[]"
- )
- row["intent"] = json.loads(row.get("intent_json") or "{}")
- return row
- finally:
- connection.close()
- def update_operator_command(command_id: str, status: str, **values: Any) -> None:
- allowed = {"confirmed_at", "executed_at", "error_message"}
- unknown = set(values) - allowed
- if unknown:
- raise ValueError(f"Unsupported operator-command fields: {sorted(unknown)}")
- assignments = ["status=%s"]
- params: list[Any] = [status]
- for column, value in values.items():
- assignments.append(f"{column}=%s")
- params.append(value)
- params.append(command_id)
- connection = connect()
- try:
- with connection.cursor() as cursor:
- cursor.execute(
- f"UPDATE operator_command SET {', '.join(assignments)} "
- "WHERE command_id=%s",
- params,
- )
- finally:
- connection.close()
- def transition_operator_command(
- command_id: str,
- *,
- expected_statuses: set[str],
- target_status: str,
- **values: Any,
- ) -> bool:
- allowed = {"confirmed_at", "executed_at", "error_message"}
- unknown = set(values) - allowed
- if unknown:
- raise ValueError(f"Unsupported operator-command fields: {sorted(unknown)}")
- if not expected_statuses:
- raise ValueError("expected_statuses must not be empty")
- assignments = ["status=%s"]
- params: list[Any] = [target_status]
- for column, value in values.items():
- assignments.append(f"{column}=%s")
- params.append(value)
- placeholders = ", ".join(["%s"] * len(expected_statuses))
- params.extend([command_id, *sorted(expected_statuses)])
- connection = connect()
- try:
- with connection.cursor() as cursor:
- affected = cursor.execute(
- f"""
- UPDATE operator_command
- SET {', '.join(assignments)}
- WHERE command_id=%s
- AND status IN ({placeholders})
- """,
- params,
- )
- return affected == 1
- finally:
- connection.close()
- def insert_operator_command_item(record: dict[str, Any]) -> None:
- columns = [
- "command_id",
- "account_id",
- "audience_name",
- "adgroup_id",
- "adgroup_name",
- "before_status",
- "target_status",
- "readback_status",
- "execution_status",
- "error_message",
- ]
- connection = connect()
- try:
- with connection.cursor() as cursor:
- cursor.execute(
- f"INSERT INTO operator_command_item ({', '.join(columns)}) "
- f"VALUES ({', '.join(['%s'] * len(columns))})",
- [record.get(column) for column in columns],
- )
- finally:
- connection.close()
- def load_operator_command_items(command_id: str) -> list[dict[str, Any]]:
- connection = connect()
- try:
- with connection.cursor() as cursor:
- cursor.execute(
- """
- SELECT * FROM operator_command_item
- WHERE command_id=%s
- ORDER BY account_id, adgroup_id, id
- """,
- (command_id,),
- )
- return list(cursor.fetchall())
- finally:
- connection.close()
- def update_operator_command_item(item_id: int, **values: Any) -> None:
- allowed = {
- "adgroup_id", "adgroup_name", "before_status", "target_status",
- "readback_status", "execution_status", "error_message",
- }
- unknown = set(values) - allowed
- if unknown:
- raise ValueError(f"Unsupported operator-command item fields: {sorted(unknown)}")
- if not values:
- return
- assignments = [f"{column}=%s" for column in values]
- connection = connect()
- try:
- with connection.cursor() as cursor:
- cursor.execute(
- f"UPDATE operator_command_item SET {', '.join(assignments)} WHERE id=%s",
- [*values.values(), item_id],
- )
- finally:
- connection.close()
- def find_pending_command_conflict(
- targets: list[tuple[int, int]],
- *,
- now: datetime,
- ) -> dict[str, Any] | None:
- if not targets:
- return None
- connection = connect()
- try:
- with connection.cursor() as cursor:
- for start in range(0, len(targets), 200):
- chunk = targets[start:start + 200]
- conditions = " OR ".join(
- ["(i.account_id=%s AND i.adgroup_id=%s)"] * len(chunk)
- )
- params: list[Any] = []
- for account_id, adgroup_id in chunk:
- params.extend([account_id, adgroup_id])
- params.append(now.replace(tzinfo=None))
- cursor.execute(
- f"""
- SELECT c.command_id, c.action, i.account_id, i.adgroup_id
- FROM operator_command c
- JOIN operator_command_item i ON i.command_id=c.command_id
- WHERE ({conditions})
- AND (
- (c.status='PENDING_CONFIRMATION' AND c.expires_at >= %s)
- OR c.status='EXECUTING'
- )
- ORDER BY c.created_at
- LIMIT 1
- """,
- params,
- )
- conflict = cursor.fetchone()
- if conflict:
- return conflict
- return None
- finally:
- connection.close()
- def list_pending_operator_commands(
- chat_id: str,
- sender_open_id: str,
- now: datetime,
- ) -> list[dict[str, Any]]:
- connection = connect()
- try:
- with connection.cursor() as cursor:
- cursor.execute(
- """
- SELECT command_id, action, preview_account_count, preview_ad_count,
- preview_cost_fen, expires_at
- FROM operator_command
- WHERE chat_id=%s AND sender_open_id=%s
- AND status='PENDING_CONFIRMATION' AND expires_at >= %s
- ORDER BY created_at
- """,
- (chat_id, sender_open_id, now.replace(tzinfo=None)),
- )
- return list(cursor.fetchall())
- finally:
- connection.close()
- def load_active_operator_draft(
- chat_id: str,
- sender_open_id: str,
- now: datetime,
- ) -> dict[str, Any] | None:
- connection = connect()
- try:
- with connection.cursor() as cursor:
- cursor.execute(
- """
- SELECT * FROM operator_command_draft
- WHERE chat_id=%s AND sender_open_id=%s
- AND status='ACTIVE' AND expires_at >= %s
- """,
- (chat_id, sender_open_id, now.replace(tzinfo=None)),
- )
- row = cursor.fetchone()
- if row:
- for field in ("account_ids", "missing_fields", "source_message_ids"):
- row[field] = json.loads(row.get(field) or "[]")
- return row
- finally:
- connection.close()
- def save_operator_draft(record: dict[str, Any]) -> dict[str, Any]:
- connection = connect()
- try:
- with connection.cursor() as cursor:
- cursor.execute(
- """
- INSERT INTO operator_command_draft
- (draft_id, chat_id, sender_open_id, raw_text, action, scope_type,
- account_ids, missing_fields, source_message_ids, status, expires_at)
- VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,'ACTIVE',%s)
- ON DUPLICATE KEY UPDATE
- draft_id=VALUES(draft_id), raw_text=VALUES(raw_text),
- action=VALUES(action),
- scope_type=VALUES(scope_type), account_ids=VALUES(account_ids),
- missing_fields=VALUES(missing_fields),
- source_message_ids=VALUES(source_message_ids),
- status='ACTIVE', expires_at=VALUES(expires_at)
- """,
- (
- record["draft_id"], record["chat_id"], record["sender_open_id"],
- record.get("raw_text"), record.get("action"),
- record.get("scope_type", "MISSING"),
- json.dumps(record.get("account_ids") or []),
- json.dumps(record.get("missing_fields") or []),
- json.dumps(record.get("source_message_ids") or []),
- record["expires_at"],
- ),
- )
- return record
- finally:
- connection.close()
- def close_operator_draft(chat_id: str, sender_open_id: str, status: str) -> None:
- if status not in {"COMPLETED", "CANCELLED", "SUPERSEDED", "EXPIRED"}:
- raise ValueError(f"Unsupported draft status: {status}")
- connection = connect()
- try:
- with connection.cursor() as cursor:
- cursor.execute(
- """
- UPDATE operator_command_draft SET status=%s
- WHERE chat_id=%s AND sender_open_id=%s AND status='ACTIVE'
- """,
- (status, chat_id, sender_open_id),
- )
- finally:
- connection.close()
- def set_operator_pause(
- *,
- account_id: int,
- adgroup_id: int,
- mode: str,
- resume_at: datetime | None,
- command_id: str,
- paused_from_status: str,
- paused_at: datetime,
- ) -> None:
- connection = connect()
- try:
- with connection.cursor() as cursor:
- cursor.execute(
- """
- UPDATE realtime_control_ad_state
- SET operator_pause_mode=%s,
- operator_resume_at=%s,
- operator_command_id=%s,
- operator_paused_from_status=%s,
- operator_paused_at=%s,
- last_action='OPERATOR_PAUSE',
- last_action_at=%s,
- last_seen_at=%s
- WHERE account_id=%s AND adgroup_id=%s
- """,
- (
- mode,
- resume_at,
- command_id,
- paused_from_status,
- paused_at,
- paused_at,
- paused_at,
- account_id,
- adgroup_id,
- ),
- )
- finally:
- connection.close()
- def clear_operator_pause(
- account_id: int,
- adgroup_id: int,
- *,
- action: str,
- action_at: datetime,
- ) -> None:
- connection = connect()
- try:
- with connection.cursor() as cursor:
- cursor.execute(
- """
- UPDATE realtime_control_ad_state
- SET operator_pause_mode=NULL,
- operator_resume_at=NULL,
- operator_command_id=NULL,
- operator_paused_from_status=NULL,
- operator_paused_at=NULL,
- last_action=%s,
- last_action_at=%s,
- last_seen_at=%s
- WHERE account_id=%s AND adgroup_id=%s
- """,
- (action, action_at, action_at, account_id, adgroup_id),
- )
- finally:
- connection.close()
- def load_operator_pauses(
- account_ids: list[int] | None = None,
- ) -> list[dict[str, Any]]:
- params: list[Any] = []
- where = "WHERE operator_pause_mode IS NOT NULL"
- if account_ids is not None:
- if not account_ids:
- return []
- where += f" AND account_id IN ({', '.join(['%s'] * len(account_ids))})"
- params.extend(account_ids)
- connection = connect()
- try:
- with connection.cursor() as cursor:
- cursor.execute(
- f"""
- SELECT *
- FROM realtime_control_ad_state
- {where}
- ORDER BY account_id, adgroup_id
- """,
- params,
- )
- return list(cursor.fetchall())
- finally:
- connection.close()
|