"""清理验证脚本留下的「软删垃圾」。 背景:三个验证脚本为了证明功能真的能用,必须往库里写真实数据。 它们跑完都会把数据删掉,但删的是**逻辑删除**(is_del=1)—— 这恰恰是 被测功能的正确行为,不该为了测试去改它。代价是每跑一轮,表里就多几行 名字带「自检 / 权限对账」的尸体。 这些尸体对业务没影响(所有查询都带 is_del=0),但演示库是要给人看的, 不该越跑越脏。所以单独开一个工具,把它们的**物理行**清掉。 默认干跑(只列不删),确认没问题再加 --yes。 用法: python verify/purge_selftest.py # 干跑,看看会删什么 python verify/purge_selftest.py --yes # 真的删 """ from __future__ import annotations import argparse import sys from pathlib import Path PROJECT_ROOT = str(Path(__file__).resolve().parent.parent) if PROJECT_ROOT not in sys.path: sys.path.insert(0, PROJECT_ROOT) from sqlalchemy import or_, select, text # noqa: E402 from app.core.database import engine # noqa: E402 from app.model import Advisor, Clazz, Employment, Score, Student, Teacher # noqa: E402 # 只认验证脚本自己造的命名,宁可漏清也不能误伤真实数据 NAME_PATTERNS = ["自检%", "%权限对账%", "%只读越权%", "%权限探针%"] def _like(col): return or_(*[col.like(p) for p in NAME_PATTERNS]) def collect() -> dict[str, list[tuple[int, str]]]: out: dict[str, list[tuple[int, str]]] = {} with engine.connect() as conn: for model in (Student, Clazz, Teacher, Advisor): rows = conn.execute(select(model.id, model.name).where(_like(model.name))).all() out[model.__tablename__] = [(r[0], r[1]) for r in rows] return out def purge(findings: dict[str, list[tuple[int, str]]]) -> dict[str, int]: deleted: dict[str, int] = {} with engine.begin() as conn: stu_ids = [i for i, _ in findings.get("student", [])] clazz_ids = [i for i, _ in findings.get("clazz", [])] teacher_ids = [i for i, _ in findings.get("teacher", [])] if stu_ids: # 先清依附于学生的行,再删学生本身,避免外键悬空 deleted["score"] = _del_in(conn, "score", "stu_id", stu_ids) deleted["employment"] = _del_in(conn, "employment", "stu_id", stu_ids) if clazz_ids: deleted["class_teachers(按班级)"] = _del_in(conn, "class_teachers", "class_id", clazz_ids) if teacher_ids: deleted["class_teachers(按老师)"] = _del_in(conn, "class_teachers", "teacher_id", teacher_ids) for table in ("student", "clazz", "teacher", "advisor"): ids = [i for i, _ in findings.get(table, [])] if ids: deleted[table] = _del_in(conn, table, "id", ids) return deleted def _del_in(conn, table: str, col: str, ids: list[int]) -> int: """按主键列表硬删。SQLAlchemy 的 IN 展开走 bindparam,避免手拼 SQL。""" stmt = text(f"DELETE FROM {table} WHERE {col} IN :ids").bindparams(ids=tuple(ids)) return conn.execute(stmt).rowcount def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--yes", action="store_true", help="真的执行删除(默认只干跑)") args = ap.parse_args() findings = collect() total = sum(len(v) for v in findings.values()) print("=" * 70) print(f"扫描验证残留 @ {engine.url.render_as_string(hide_password=True)}") print("=" * 70) for table, rows in findings.items(): if not rows: continue print(f"\n{table}({len(rows)} 条)") for i, name in rows: print(f" id={i:<6} {name}") if not total: print("\n没有残留,库是干净的。") return 0 print(f"\n合计 {total} 条待清理。") if not args.yes: print("这是干跑。确认无误后加 --yes 真删。") return 0 deleted = purge(findings) print("\n已物理删除:") for table, n in deleted.items(): if n: print(f" {table:<24} {n} 行") print("\n清理完成。") return 0 if __name__ == "__main__": sys.exit(main())