Files
test/verify/purge_selftest.py
T
2026-09-21 19:03:31 +08:00

117 lines
4.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""清理验证脚本留下的「软删垃圾」。
背景:三个验证脚本为了证明功能真的能用,必须往库里写真实数据。
它们跑完都会把数据删掉,但删的是**逻辑删除**(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())