| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213 |
- """SQLite 持久化:注册成功的账号、session 快照、CPA 上传记录。"""
- from __future__ import annotations
- import json
- import os
- import sqlite3
- import threading
- import time
- from contextlib import contextmanager
- from typing import Any, Iterable
- DATA_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data")
- DB_PATH = os.path.join(DATA_DIR, "accounts.db")
- _LOCK = threading.Lock()
- SCHEMA = """
- CREATE TABLE IF NOT EXISTS accounts (
- email TEXT PRIMARY KEY,
- password TEXT NOT NULL,
- created_at INTEGER NOT NULL,
- updated_at INTEGER NOT NULL,
- plan_type TEXT,
- final_status TEXT, -- registered/paid/plus/cpa_uploaded/failed/stopped
- last_error TEXT,
- long_link TEXT,
- cpa_file_name TEXT,
- cpa_uploaded_at INTEGER,
- initial_session_json TEXT, -- 注册成功时拉到的 /api/auth/session
- plus_session_json TEXT, -- 付款后拉到的 plus session
- notes TEXT
- );
- CREATE INDEX IF NOT EXISTS idx_accounts_status ON accounts(final_status);
- CREATE INDEX IF NOT EXISTS idx_accounts_created ON accounts(created_at);
- CREATE TABLE IF NOT EXISTS account_events (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- email TEXT NOT NULL,
- ts INTEGER NOT NULL,
- stage TEXT NOT NULL,
- status TEXT NOT NULL, -- info/ok/warn/error
- detail TEXT,
- payload_json TEXT
- );
- CREATE INDEX IF NOT EXISTS idx_events_email ON account_events(email);
- CREATE INDEX IF NOT EXISTS idx_events_ts ON account_events(ts);
- """
- def _now_ms() -> int:
- return int(time.time() * 1000)
- def _ensure_dir():
- os.makedirs(DATA_DIR, exist_ok=True)
- def init_db():
- _ensure_dir()
- with _LOCK, sqlite3.connect(DB_PATH) as conn:
- conn.executescript(SCHEMA)
- conn.commit()
- @contextmanager
- def _conn():
- _ensure_dir()
- with _LOCK:
- c = sqlite3.connect(DB_PATH)
- c.row_factory = sqlite3.Row
- try:
- yield c
- c.commit()
- finally:
- c.close()
- def _dump(value: Any) -> str | None:
- if value is None:
- return None
- try:
- return json.dumps(value, ensure_ascii=False)
- except Exception:
- return str(value)
- def upsert_account(email: str, password: str, *, fields: dict | None = None) -> dict:
- """Insert or update by email. fields 中只更新非 None 字段。"""
- init_db()
- fields = dict(fields or {})
- now = _now_ms()
- with _conn() as c:
- row = c.execute("SELECT email FROM accounts WHERE email = ?", (email,)).fetchone()
- if row is None:
- c.execute(
- """
- INSERT INTO accounts (email, password, created_at, updated_at, plan_type,
- final_status, last_error, long_link, cpa_file_name, cpa_uploaded_at,
- initial_session_json, plus_session_json, notes)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
- """,
- (
- email, password, now, now,
- fields.get("plan_type"),
- fields.get("final_status") or "registered",
- fields.get("last_error"),
- fields.get("long_link"),
- fields.get("cpa_file_name"),
- fields.get("cpa_uploaded_at"),
- _dump(fields.get("initial_session")) if "initial_session" in fields else fields.get("initial_session_json"),
- _dump(fields.get("plus_session")) if "plus_session" in fields else fields.get("plus_session_json"),
- fields.get("notes"),
- ),
- )
- else:
- sets = ["updated_at = ?"]
- args: list[Any] = [now]
- for col in ("plan_type", "final_status", "last_error", "long_link",
- "cpa_file_name", "cpa_uploaded_at", "notes"):
- if col in fields and fields[col] is not None:
- sets.append(f"{col} = ?")
- args.append(fields[col])
- if "initial_session" in fields:
- sets.append("initial_session_json = ?")
- args.append(_dump(fields["initial_session"]))
- elif "initial_session_json" in fields and fields["initial_session_json"] is not None:
- sets.append("initial_session_json = ?")
- args.append(fields["initial_session_json"])
- if "plus_session" in fields:
- sets.append("plus_session_json = ?")
- args.append(_dump(fields["plus_session"]))
- elif "plus_session_json" in fields and fields["plus_session_json"] is not None:
- sets.append("plus_session_json = ?")
- args.append(fields["plus_session_json"])
- if password:
- sets.append("password = ?")
- args.append(password)
- args.append(email)
- c.execute(f"UPDATE accounts SET {', '.join(sets)} WHERE email = ?", args)
- return _row_to_dict(c.execute("SELECT * FROM accounts WHERE email = ?", (email,)).fetchone())
- def add_event(email: str, stage: str, status: str = "info",
- detail: str | None = None, payload: Any = None) -> int:
- init_db()
- with _conn() as c:
- cur = c.execute(
- "INSERT INTO account_events (email, ts, stage, status, detail, payload_json) VALUES (?, ?, ?, ?, ?, ?)",
- (email or "", _now_ms(), stage, status, detail, _dump(payload)),
- )
- return cur.lastrowid
- def list_accounts(limit: int = 200, status: str | None = None) -> list[dict]:
- init_db()
- with _conn() as c:
- if status:
- rows = c.execute(
- "SELECT * FROM accounts WHERE final_status = ? ORDER BY created_at DESC LIMIT ?",
- (status, limit),
- ).fetchall()
- else:
- rows = c.execute(
- "SELECT * FROM accounts ORDER BY created_at DESC LIMIT ?",
- (limit,),
- ).fetchall()
- return [_row_to_dict(r) for r in rows]
- def get_account(email: str) -> dict | None:
- init_db()
- with _conn() as c:
- r = c.execute("SELECT * FROM accounts WHERE email = ?", (email,)).fetchone()
- return _row_to_dict(r) if r else None
- def list_events(email: str, limit: int = 100) -> list[dict]:
- init_db()
- with _conn() as c:
- rows = c.execute(
- "SELECT * FROM account_events WHERE email = ? ORDER BY ts DESC LIMIT ?",
- (email, limit),
- ).fetchall()
- return [_event_row(r) for r in rows]
- def _row_to_dict(row: sqlite3.Row | None) -> dict | None:
- if row is None:
- return None
- d = {k: row[k] for k in row.keys()}
- # session JSON 反序列化但保留 raw 副本
- for k in ("initial_session_json", "plus_session_json"):
- raw = d.get(k)
- if raw:
- try:
- d[k.replace("_json", "")] = json.loads(raw)
- except Exception:
- d[k.replace("_json", "")] = None
- return d
- def _event_row(row: sqlite3.Row) -> dict:
- d = {k: row[k] for k in row.keys()}
- raw = d.get("payload_json")
- if raw:
- try:
- d["payload"] = json.loads(raw)
- except Exception:
- d["payload"] = None
- return d
|