c2f4c13ab3
- users.phone/wx_openid/wx_mini_openid/wx_unionid 各自唯一(非空部分唯一索引) - 创建账号必生成ID(create用new_id);find_by_phone/find_by_wx 唯一校验 - cleanup_accounts.py 清理旧accounts已被users接管的重复账号;seed不再重复造账号、迁移后删旧行
114 lines
4.4 KiB
Python
114 lines
4.4 KiB
Python
# -*- 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()
|