# -*- coding: utf-8 -*- """清理数据库中的多余账号(非运行态,迁移/种子前执行)。 用户 ID / 登录名 / 手机号 是三种独立逻辑,各自唯一;账号创建时 ID 必存在。 历史遗留:培训端旧 ``accounts`` 表是整套平行账号仓(同名/同手机/同微信身份在 ``users`` 已有规范账号)——这些就是「多余账号」。本脚本: 1. 安全删除旧 ``accounts`` 中已被 ``users`` 规范账号接管(user_id/登录名/手机号/ 微信身份任一命中)的行; 2. 校验 ``users`` 无重复 登录名/手机号/微信 openid(非空)——为迁移 0011 唯一索引清障; 3. 幂等:重跑安全。 用法:uv run python scripts/db/cleanup_accounts.py 配合:先 cleanup(清多余+去重)→ 再 migrate.py(唯一索引)→ seed.py。 """ from __future__ import annotations import sqlite3 import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent)) from app import config from app.infrastructure.repositories import new_id # noqa: F401 (确保路径/依赖就绪) LEGACY_TABLES = ("accounts",) def _dup_groups(cur: sqlite3.Cursor, col: str) -> list[tuple]: """返回 users 中某列非空重复的分组((值, 计数, [id...]))。""" cur.execute( f"SELECT {col}, COUNT(*) c FROM users WHERE {col} != '' GROUP BY {col} HAVING c > 1" ) return [(row[0], row[1]) for row in cur.fetchall()] def cleanup_users_duplicates(conn: sqlite3.Connection) -> list[str]: """校验 users 唯一性;存在重复非空值则提示手动处理(返回描述)。""" cur = conn.cursor() problems: list[str] = [] for col in ("username", "phone", "wx_openid", "wx_mini_openid", "wx_unionid"): groups = _dup_groups(cur, col) if groups: for val, cnt in groups: problems.append(f"users.{col}={val!r} 重复 {cnt} 次") return problems def cleanup_legacy_accounts(conn: sqlite3.Connection) -> int: """删除已被 users 接管的旧 accounts 行,返回删除数。""" cur = conn.cursor() try: cur.execute("PRAGMA table_info(accounts)") cols = [r[1] for r in cur.fetchall()] except sqlite3.OperationalError: return 0 if "username" not in cols: return 0 rows = cur.execute("SELECT id,username,phone,wxid FROM accounts").fetchall() deleted = 0 for acct_id, username, phone, wxid in rows: uname = (username or "").strip() ph = (phone or "").strip() wx = (wxid or "").strip() hit = False if uname: cur.execute("SELECT 1 FROM users WHERE username=? LIMIT 1", (uname,)) hit = hit or cur.fetchone() is not None if not hit and ph: cur.execute("SELECT 1 FROM users WHERE phone=? LIMIT 1", (ph,)) hit = hit or cur.fetchone() is not None if not hit and wx: cur.execute( "SELECT 1 FROM users WHERE wx_mini_openid=? OR wx_openid=? LIMIT 1", (wx, wx), ) hit = hit or cur.fetchone() is not None if hit: cur.execute("DELETE FROM accounts WHERE id=?", (acct_id,)) deleted += 1 conn.commit() return deleted def main() -> None: conn = sqlite3.connect(config.DATABASE_URL.replace("sqlite+aiosqlite:///", "")) try: dup = cleanup_users_duplicates(conn) if dup: print("⚠️ users 存在重复非空唯一值(需先手动核对):") for d in dup: print(" -", d) print("请先人工确认归属后处理,再执行迁移唯一索引。") else: print("✅ users 唯一性检查通过(无重复 登录名/手机号/微信身份)") for tbl in LEGACY_TABLES: try: deleted = cleanup_legacy_accounts(conn) if deleted: print(f"✅ 已清理多余账号:{tbl} 移除 {deleted} 条已被 users 接管的行") except sqlite3.OperationalError as exc: print(f"⚠️ {tbl} 清理跳过:{exc}") # 清空后的 accounts 若为空即视为多余账号仓彻底闲置 n = conn.execute("SELECT COUNT(*) FROM accounts").fetchone()[0] print(f"旧 accounts 表剩余 {n} 条(0=已彻底清理)") finally: conn.close() if __name__ == "__main__": main()