"""MySQL persistence for versioned ROI runs, snapshots, and action audit.""" from __future__ import annotations import json from datetime import date, datetime from decimal import Decimal from typing import Any, Iterable from storage import connect FINAL_STATUSES = { "COMPLETED", "PARTIAL", "REJECTED", "EXPIRED", } def _is_expired(expires_at: datetime | None, now: datetime) -> bool: if expires_at is None: return False if expires_at.tzinfo is None and now.tzinfo is not None: now = now.replace(tzinfo=None) elif expires_at.tzinfo is not None and now.tzinfo is None: expires_at = expires_at.replace(tzinfo=None) return now > expires_at def json_dumps(value: Any) -> str: def default(item: Any) -> Any: if isinstance(item, (datetime, date)): return item.isoformat() if isinstance(item, Decimal): return str(item) if hasattr(item, "item"): return item.item() if hasattr(item, "tolist"): return item.tolist() raise TypeError(f"Unsupported JSON value: {type(item)!r}") return json.dumps(value, ensure_ascii=False, default=default) def publish_fission_parameter_release( release: dict[str, Any], values: Iterable[dict[str, Any]], *, published_by: str, ) -> bool: """Publish an immutable parameter version; return False if already present.""" value_rows = list(values) connection = connect() connection.autocommit(False) try: with connection.cursor() as cursor: cursor.execute( "SELECT content_sha256 FROM roi_fission_parameter_release " "WHERE version=%s FOR UPDATE", (release["version"],), ) existing = cursor.fetchone() if existing: if existing["content_sha256"] != release["content_sha256"]: raise ValueError( "传播裂变系数版本已存在且内容不同,禁止覆盖: " f"{release['version']}" ) connection.rollback() return False cursor.execute( """ INSERT INTO roi_fission_parameter_release (version, cohort_date, observation_end_date, horizon_days, run_suffix, content_sha256, metadata_json, published_by) VALUES (%s,%s,%s,%s,%s,%s,%s,%s) """, ( release["version"], release["cohort_date"], release["observation_end_date"], release["horizon_days"], release["run_suffix"], release["content_sha256"], json_dumps(release["metadata"]), published_by, ), ) cursor.executemany( """ INSERT INTO roi_fission_parameter_value (version, entity_type, match_level, key_primary, key_secondary, multiplier, multiplier_vs_first) VALUES (%s,%s,%s,%s,%s,%s,%s) """, [ ( release["version"], row["entity_type"], row["match_level"], row["key_primary"], row["key_secondary"], row["multiplier"], row.get("multiplier_vs_first"), ) for row in value_rows ], ) connection.commit() return True except Exception: connection.rollback() raise finally: connection.close() def load_fission_parameter_release( version: str, ) -> tuple[dict[str, Any], list[dict[str, Any]]]: connection = connect() try: with connection.cursor() as cursor: cursor.execute( "SELECT * FROM roi_fission_parameter_release WHERE version=%s", (version,), ) release = cursor.fetchone() if not release: raise ValueError(f"数据库不存在传播裂变系数版本: {version}") cursor.execute( """ SELECT entity_type, match_level, key_primary, key_secondary, multiplier, multiplier_vs_first FROM roi_fission_parameter_value WHERE version=%s ORDER BY entity_type, match_level, key_primary, key_secondary """, (version,), ) return release, list(cursor.fetchall()) finally: connection.close() def create_or_load_run(record: dict[str, Any]) -> dict[str, Any]: connection = connect() try: with connection.cursor() as cursor: cursor.execute( """ INSERT IGNORE INTO roi_metric_run (run_id, run_key, metric_version, policy_version, fission_parameter_version, fission_cohort_date, start_date, end_date, config_json, status) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,'COMPUTING') """, ( record["run_id"], record["run_key"], record["metric_version"], record["policy_version"], record["fission_parameter_version"], record["fission_cohort_date"], record["start_date"], record["end_date"], json_dumps(record["config"]), ), ) cursor.execute( "SELECT * FROM roi_metric_run WHERE run_key=%s", (record["run_key"],), ) run = cursor.fetchone() if run: return run cursor.execute( "SELECT * FROM roi_metric_run WHERE run_id=%s", (record["run_id"],), ) collision = cursor.fetchone() if collision: raise RuntimeError( f"ROI run_id already exists with a different run_key: " f"{record['run_id']}" ) return {} finally: connection.close() def load_run(run_id: str) -> dict[str, Any] | None: connection = connect() try: with connection.cursor() as cursor: cursor.execute("SELECT * FROM roi_metric_run WHERE run_id=%s", (run_id,)) return cursor.fetchone() finally: connection.close() def replace_run_results( run_id: str, *, snapshots: Iterable[dict[str, Any]], actions: Iterable[dict[str, Any]], thresholds: dict[str, Any], ) -> tuple[int, int]: snapshot_rows = list(snapshots) action_rows = list(actions) connection = connect() connection.autocommit(False) try: with connection.cursor() as cursor: cursor.execute( "SELECT status FROM roi_metric_run WHERE run_id=%s FOR UPDATE", (run_id,), ) run = cursor.fetchone() if not run: raise ValueError(f"ROI run not found: {run_id}") if run["status"] not in {"COMPUTING", "COMPUTED", "FAILED"}: raise RuntimeError( f"ROI run {run_id} cannot be replaced in status {run['status']}" ) cursor.execute("DELETE FROM roi_action_item WHERE run_id=%s", (run_id,)) cursor.execute("DELETE FROM roi_entity_snapshot WHERE run_id=%s", (run_id,)) snapshot_columns = [ "run_id", "entity_hash", "entity_type", "channel", "account_id", "account_name", "adgroup_id", "adgroup_name", "dynamic_creative_id", "audience_name", "conversion_goal", "partner_name", "official_account_name", "fission_parameter_version", "fission_cohort_date", "fission_multiplier_vs_t0", "fission_multiplier_vs_first", "fission_match_level", "fission_source", "ad_age", "avg_first_uv", "first_uv", "t0_fission_count", "t0_fission_rate", "cost", "efficiency_revenue", "fission_revenue", "actual_total_revenue", "actual_roi", "predicted_tail_revenue", "predicted_fission_revenue", "total_revenue", "roi", "stop_threshold", "scale_threshold", "recommended_action", "action_reason", "execution_mode", "ineligible_reason", "daily_metrics_json", ] if snapshot_rows: cursor.executemany( f"INSERT INTO roi_entity_snapshot ({', '.join(snapshot_columns)}) " f"VALUES ({', '.join(['%s'] * len(snapshot_columns))})", [ [ json_dumps(row[column]) if column == "daily_metrics_json" else row.get(column) for column in snapshot_columns ] for row in snapshot_rows ], ) action_columns = [ "run_id", "idempotency_key", "action_type", "account_id", "adgroup_id", "dynamic_creative_id", "execution_status", ] if action_rows: cursor.executemany( f"INSERT INTO roi_action_item ({', '.join(action_columns)}) " f"VALUES ({', '.join(['%s'] * len(action_columns))})", [ [row.get(column) for column in action_columns] for row in action_rows ], ) cursor.execute( """ UPDATE roi_metric_run SET thresholds_json=%s, status='COMPUTED', entity_count=%s, candidate_count=%s, actionable_count=%s, error_message=NULL WHERE run_id=%s """, ( json_dumps(thresholds), len(snapshot_rows), sum(bool(row.get("recommended_action")) for row in snapshot_rows), len(action_rows), run_id, ), ) connection.commit() return len(snapshot_rows), len(action_rows) except Exception: connection.rollback() raise finally: connection.close() def mark_published( run_id: str, *, sheet_token: str, sheet_url: str, message_id: str, expires_at: datetime | None, requires_approval: bool, ) -> None: connection = connect() try: with connection.cursor() as cursor: affected = cursor.execute( """ UPDATE roi_metric_run SET status=%s, sheet_token=%s, sheet_url=%s, message_id=%s, expires_at=%s, error_message=NULL WHERE run_id=%s AND status IN ('COMPUTED','PENDING_APPROVAL') """, ( "PENDING_APPROVAL" if requires_approval else "COMPLETED", sheet_token, sheet_url, message_id, expires_at, run_id, ), ) if not affected: raise RuntimeError(f"ROI run cannot be published: {run_id}") finally: connection.close() def mark_failed(run_id: str, error_message: str) -> None: connection = connect() try: with connection.cursor() as cursor: cursor.execute( """ UPDATE roi_metric_run SET status='FAILED', error_message=%s WHERE run_id=%s AND status NOT IN ('COMPLETED','PARTIAL','REJECTED') """, (error_message[:4000], run_id), ) finally: connection.close() def upsert_agency_delivery(record: dict[str, Any]) -> dict[str, Any]: """Create or refresh one idempotent agency delivery record.""" connection = connect() try: with connection.cursor() as cursor: cursor.execute( """ INSERT INTO roi_agency_delivery (run_id, agency_name, agency_report_version, file_path, file_sha256, creative_rows, ad_rows, route_fingerprint, status) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,'PENDING') ON DUPLICATE KEY UPDATE status=CASE WHEN status='SENT' THEN status WHEN NOT (route_fingerprint <=> VALUES(route_fingerprint)) THEN 'PENDING' ELSE status END, error_message=CASE WHEN NOT (route_fingerprint <=> VALUES(route_fingerprint)) THEN NULL ELSE error_message END, file_path=IF(status='SENT', file_path, VALUES(file_path)), file_sha256=IF(status='SENT', file_sha256, VALUES(file_sha256)), creative_rows=VALUES(creative_rows), ad_rows=VALUES(ad_rows), route_fingerprint=VALUES(route_fingerprint) """, ( record["run_id"], record["agency_name"], record["agency_report_version"], record["file_path"], record["file_sha256"], int(record.get("creative_rows") or 0), int(record.get("ad_rows") or 0), record.get("route_fingerprint"), ), ) cursor.execute( """ SELECT * FROM roi_agency_delivery WHERE run_id=%s AND agency_name=%s AND agency_report_version=%s """, ( record["run_id"], record["agency_name"], record["agency_report_version"], ), ) return cursor.fetchone() or {} finally: connection.close() def load_agency_deliveries(run_id: str) -> list[dict[str, Any]]: connection = connect() try: with connection.cursor() as cursor: cursor.execute( """ SELECT * FROM roi_agency_delivery WHERE run_id=%s ORDER BY agency_name """, (run_id,), ) return list(cursor.fetchall()) finally: connection.close() def update_agency_delivery( delivery_id: int, *, status: str, sheet_token: str | None = None, sheet_url: str | None = None, response_code: str | None = None, error_message: str | None = None, sent_at: datetime | None = None, increment_attempt: bool = False, ) -> None: allowed_statuses = {"PENDING", "UPLOADED", "SENT", "SKIPPED", "FAILED"} if status not in allowed_statuses: raise ValueError(f"Unsupported agency delivery status: {status}") connection = connect() try: with connection.cursor() as cursor: cursor.execute( """ UPDATE roi_agency_delivery SET status=%s, sheet_token=COALESCE(%s, sheet_token), sheet_url=COALESCE(%s, sheet_url), response_code=%s, error_message=%s, sent_at=COALESCE(%s, sent_at), attempt_count=attempt_count+%s WHERE id=%s """, ( status, sheet_token, sheet_url, response_code, error_message[:4000] if error_message else None, sent_at, 1 if increment_attempt else 0, delivery_id, ), ) finally: connection.close() def reject_run(run_id: str, *, sender_open_id: str, now: datetime) -> dict[str, Any]: connection = connect() connection.autocommit(False) try: with connection.cursor() as cursor: cursor.execute( "SELECT * FROM roi_metric_run WHERE run_id=%s FOR UPDATE", (run_id,), ) run = cursor.fetchone() if not run: raise ValueError(f"ROI批次不存在: {run_id}") if run["status"] in FINAL_STATUSES: connection.commit() return run if run["status"] != "PENDING_APPROVAL": raise RuntimeError(f"ROI批次当前不可拒绝: {run['status']}") if _is_expired(run.get("expires_at"), now): cursor.execute( "UPDATE roi_metric_run SET status='EXPIRED' WHERE run_id=%s", (run_id,), ) else: cursor.execute( """ UPDATE roi_metric_run SET status='REJECTED', rejected_by=%s, rejected_at=%s WHERE run_id=%s """, (sender_open_id, now, run_id), ) connection.commit() return load_run(run_id) or run except Exception: connection.rollback() raise finally: connection.close() def claim_run_for_execution( run_id: str, *, sender_open_id: str, now: datetime, ) -> dict[str, Any]: connection = connect() connection.autocommit(False) try: with connection.cursor() as cursor: cursor.execute( "SELECT * FROM roi_metric_run WHERE run_id=%s FOR UPDATE", (run_id,), ) run = cursor.fetchone() if not run: raise ValueError(f"ROI批次不存在: {run_id}") if run["status"] in FINAL_STATUSES: connection.commit() return run if run["status"] == "PENDING_APPROVAL": if _is_expired(run.get("expires_at"), now): cursor.execute( "UPDATE roi_metric_run SET status='EXPIRED' WHERE run_id=%s", (run_id,), ) connection.commit() return load_run(run_id) or run cursor.execute( """ UPDATE roi_metric_run SET status='EXECUTING', approved_by=%s, approved_at=%s WHERE run_id=%s """, (sender_open_id, now, run_id), ) elif run["status"] != "EXECUTING": raise RuntimeError(f"ROI批次当前不可确认: {run['status']}") connection.commit() return load_run(run_id) or run except Exception: connection.rollback() raise finally: connection.close() def load_action_items(run_id: str) -> list[dict[str, Any]]: connection = connect() try: with connection.cursor() as cursor: cursor.execute( "SELECT * FROM roi_action_item WHERE run_id=%s ORDER BY id", (run_id,), ) return list(cursor.fetchall()) finally: connection.close() def load_pending_sheet_runs(now: datetime) -> list[dict[str, Any]]: connection = connect() try: with connection.cursor() as cursor: cursor.execute( """ SELECT * FROM roi_metric_run WHERE status='PENDING_APPROVAL' AND sheet_token IS NOT NULL AND (expires_at IS NULL OR expires_at>=%s) ORDER BY created_at """, (now,), ) return list(cursor.fetchall()) finally: connection.close() def expire_pending_sheet_runs(now: datetime) -> int: connection = connect() connection.autocommit(False) try: with connection.cursor() as cursor: cursor.execute( """ SELECT run_id FROM roi_metric_run WHERE status='PENDING_APPROVAL' AND expires_at IS NOT NULL AND expires_at<%s FOR UPDATE """, (now,), ) run_ids = [row["run_id"] for row in cursor.fetchall()] if run_ids: placeholders = ",".join(["%s"] * len(run_ids)) cursor.execute( f""" UPDATE roi_action_item SET approval_status='EXPIRED', execution_status='EXPIRED', executed_at=%s WHERE run_id IN ({placeholders}) AND approval_status='PENDING' """, [now, *run_ids], ) cursor.execute( f""" UPDATE roi_metric_run SET status='EXPIRED' WHERE run_id IN ({placeholders}) """, run_ids, ) connection.commit() return len(run_ids) except Exception: connection.rollback() raise finally: connection.close() def record_sheet_decision( *, run_id: str, idempotency_key: str, decision: str, sheet_row_number: int, now: datetime, ) -> dict[str, Any] | None: if decision not in {"APPROVED", "REJECTED"}: raise ValueError(f"Unsupported ROI sheet decision: {decision}") connection = connect() connection.autocommit(False) try: with connection.cursor() as cursor: cursor.execute( "SELECT status, expires_at FROM roi_metric_run " "WHERE run_id=%s FOR UPDATE", (run_id,), ) run = cursor.fetchone() if not run or run["status"] != "PENDING_APPROVAL": connection.rollback() return None if _is_expired(run.get("expires_at"), now): connection.rollback() return None cursor.execute( """ SELECT * FROM roi_action_item WHERE run_id=%s AND idempotency_key=%s FOR UPDATE """, (run_id, idempotency_key), ) item = cursor.fetchone() if not item: connection.rollback() return None if item["approval_status"] != "PENDING": connection.commit() return {**item, "decision_changed": False} if decision == "APPROVED": cursor.execute( """ UPDATE roi_action_item SET approval_status='APPROVED', approval_source='FEISHU_SHEET', approved_at=%s, sheet_row_number=%s WHERE id=%s AND approval_status='PENDING' """, (now, sheet_row_number, item["id"]), ) else: cursor.execute( """ UPDATE roi_action_item SET approval_status='REJECTED', approval_source='FEISHU_SHEET', rejected_at=%s, sheet_row_number=%s, execution_status='REJECTED', executed_at=%s WHERE id=%s AND approval_status='PENDING' """, (now, sheet_row_number, now, item["id"]), ) connection.commit() return { **item, "approval_status": decision, "sheet_row_number": sheet_row_number, "decision_changed": True, } except Exception: connection.rollback() raise finally: connection.close() def load_actions_by_ids(item_ids: Iterable[int]) -> list[dict[str, Any]]: ids = [int(value) for value in item_ids] if not ids: return [] connection = connect() try: with connection.cursor() as cursor: placeholders = ",".join(["%s"] * len(ids)) cursor.execute( f"SELECT * FROM roi_action_item WHERE id IN ({placeholders}) ORDER BY id", ids, ) return list(cursor.fetchall()) finally: connection.close() def load_unnotified_action_details( run_ids: Iterable[str] | None = None, ) -> list[dict[str, Any]]: values = [str(value) for value in run_ids] if run_ids is not None else [] run_filter = "" params: list[Any] = [] if run_ids is not None: if not values: return [] placeholders = ",".join(["%s"] * len(values)) run_filter = f"AND a.run_id IN ({placeholders})" params.extend(values) connection = connect() try: with connection.cursor() as cursor: cursor.execute( f""" SELECT a.*, r.sheet_url, s.adgroup_name, s.audience_name, s.cost, s.roi FROM roi_action_item a JOIN roi_metric_run r ON r.run_id=a.run_id LEFT JOIN roi_entity_snapshot s ON s.id=( SELECT MIN(s2.id) FROM roi_entity_snapshot s2 WHERE s2.run_id=a.run_id AND s2.account_id=a.account_id AND s2.adgroup_id=a.adgroup_id AND ( a.dynamic_creative_id IS NULL OR s2.dynamic_creative_id=a.dynamic_creative_id ) ) WHERE a.approval_status IN ('APPROVED','REJECTED') AND a.execution_status NOT IN ('PENDING','PREPARED') AND a.result_notified_at IS NULL {run_filter} ORDER BY a.run_id, a.id LIMIT 100 """, params, ) return list(cursor.fetchall()) finally: connection.close() def mark_action_notifications( item_ids: Iterable[int], *, notified_at: datetime | None, error: str | None, ) -> None: ids = [int(value) for value in item_ids] if not ids: return connection = connect() try: with connection.cursor() as cursor: placeholders = ",".join(["%s"] * len(ids)) cursor.execute( f""" UPDATE roi_action_item SET result_notified_at=%s, notification_error=%s WHERE id IN ({placeholders}) """, [notified_at, error[:4000] if error else None, *ids], ) finally: connection.close() def finalize_sheet_run_if_resolved(run_id: str, *, now: datetime) -> dict[str, Any]: connection = connect() try: with connection.cursor() as cursor: cursor.execute( """ SELECT SUM(approval_status='PENDING') AS pending_approvals, SUM(execution_status='SUCCESS') AS successes, SUM(execution_status IN ('FAILED','OUTCOME_UNKNOWN','VERIFY_FAILED')) AS failures, SUM(execution_status LIKE 'SKIPPED%%') AS skipped, SUM(approval_status='REJECTED') AS rejected FROM roi_action_item WHERE run_id=%s """, (run_id,), ) counts = cursor.fetchone() or {} normalized = {key: int(value or 0) for key, value in counts.items()} if normalized["pending_approvals"]: return {"status": "PENDING_APPROVAL", **normalized} status = "PARTIAL" if normalized["failures"] else "COMPLETED" cursor.execute( "UPDATE roi_metric_run SET status=%s, executed_at=%s WHERE run_id=%s", (status, now, run_id), ) return {"status": status, **normalized} finally: connection.close() def update_action_item(item_id: int, **values: Any) -> None: allowed = { "bid_field", "initial_base_bid_fen", "base_bid_fen", "before_bid_fen", "target_bid_fen", "before_status", "target_status", "readback_status", "execution_status", "skip_reason", "error_message", "pre_state_json", "readback_json", "executed_at", "notification_error", } unknown = set(values) - allowed if unknown: raise ValueError(f"Unsupported ROI action fields: {sorted(unknown)}") assignments: list[str] = [] params: list[Any] = [] for column, value in values.items(): assignments.append(f"{column}=%s") params.append( json_dumps(value) if column in {"pre_state_json", "readback_json"} and value is not None else value ) if not assignments: return params.append(item_id) connection = connect() try: with connection.cursor() as cursor: cursor.execute( f"UPDATE roi_action_item SET {', '.join(assignments)} WHERE id=%s", params, ) finally: connection.close() def finalize_run(run_id: str, *, now: datetime) -> dict[str, Any]: connection = connect() try: with connection.cursor() as cursor: cursor.execute( """ SELECT SUM(execution_status='SUCCESS') AS successes, SUM(execution_status IN ('FAILED','OUTCOME_UNKNOWN','VERIFY_FAILED')) AS failures, SUM(execution_status LIKE 'SKIPPED%%') AS skipped FROM roi_action_item WHERE run_id=%s """, (run_id,), ) counts = cursor.fetchone() or {} failures = int(counts.get("failures") or 0) status = "PARTIAL" if failures else "COMPLETED" cursor.execute( "UPDATE roi_metric_run SET status=%s, executed_at=%s WHERE run_id=%s", (status, now, run_id), ) return {"status": status, **{key: int(value or 0) for key, value in counts.items()}} finally: connection.close()