baseline: 批次A-D 成果 + membership 半成品(测试红)
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
"""迁移健康检查(独立脚本,需在真实库上运行)。
|
||||
|
||||
为什么不是 pytest 用例:pytest-django 的测试库按**模型**建表并绕过迁移状态,
|
||||
`MigrationAutodetector` 在 pytest 进程里返回空——"模型改了没生成迁移"这类问题
|
||||
在单元测试里**测不出来**(实测过,会假通过)。
|
||||
|
||||
本脚本用真实 settings 跑,等价于 `makemigrations --check` + `migrate --check`,
|
||||
但额外做几件 pytest 做不到的事:
|
||||
1. 检查模型字段与**真实库**列是否一致(能抓到"迁移生成了但没 migrate")
|
||||
2. 检查新 app 的表是否真存在
|
||||
3. 退出码非 0,可直接接 CI / 部署前钩子
|
||||
|
||||
用法:
|
||||
python scripts/check_migrations.py # 用 DJANGO_SETTINGS_MODULE(默认 dev)
|
||||
DJANGO_SETTINGS_MODULE=config.settings.prod python scripts/check_migrations.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# 让脚本能 import 到项目(backend/ 为根)
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.dev")
|
||||
|
||||
import django # noqa: E402
|
||||
|
||||
django.setup()
|
||||
|
||||
from django.apps import apps as django_apps # noqa: E402
|
||||
from django.core.management import call_command # noqa: E402
|
||||
from django.db import connection # noqa: E402
|
||||
from io import StringIO # noqa: E402
|
||||
|
||||
|
||||
OWN_APPS = [
|
||||
"core", "catalog", "partner", "inventory", "finance",
|
||||
"purchase", "sales", "report", "channel", "notify",
|
||||
"openapi", "printing", "ai", "billing", "website", "storefront",
|
||||
]
|
||||
|
||||
RED = "\033[31m"
|
||||
GREEN = "\033[32m"
|
||||
YELLOW = "\033[33m"
|
||||
RESET = "\033[0m"
|
||||
|
||||
|
||||
def ok(msg: str):
|
||||
print(f" {GREEN}✓{RESET} {msg}")
|
||||
|
||||
|
||||
def bad(msg: str):
|
||||
print(f" {RED}✗{RESET} {msg}")
|
||||
|
||||
|
||||
def warn(msg: str):
|
||||
print(f" {YELLOW}!{RESET} {msg}")
|
||||
|
||||
|
||||
def check_pending_migrations() -> int:
|
||||
"""1. 模型改了但没 makemigrations。"""
|
||||
print("\n[1] 待生成的迁移(makemigrations --check)")
|
||||
out = StringIO()
|
||||
try:
|
||||
call_command("makemigrations", "--check", "--dry-run",
|
||||
stdout=out, stderr=out, verbosity=1)
|
||||
ok("无待生成迁移")
|
||||
return 0
|
||||
except SystemExit as exc:
|
||||
if exc.code == 0:
|
||||
ok("无待生成迁移")
|
||||
return 0
|
||||
text = out.getvalue().strip()
|
||||
bad("存在未生成的迁移:")
|
||||
for line in text.splitlines():
|
||||
print(" " + line)
|
||||
print(f"\n 修复:python manage.py makemigrations")
|
||||
return 1
|
||||
|
||||
|
||||
def check_unapplied_migrations() -> int:
|
||||
"""2. 有迁移文件但没执行 migrate。"""
|
||||
print("\n[2] 未应用的迁移(migrate --check)")
|
||||
out = StringIO()
|
||||
try:
|
||||
call_command("migrate", "--check", stdout=out, stderr=out, verbosity=0)
|
||||
ok("所有迁移已应用")
|
||||
return 0
|
||||
except SystemExit as exc:
|
||||
if exc.code == 0:
|
||||
ok("所有迁移已应用")
|
||||
return 0
|
||||
text = out.getvalue().strip()
|
||||
bad("存在未应用的迁移:")
|
||||
for line in text.splitlines()[:20]:
|
||||
print(" " + line)
|
||||
print(f"\n 修复:python manage.py migrate")
|
||||
return 1
|
||||
|
||||
|
||||
def check_model_columns() -> int:
|
||||
"""3. 模型字段 vs 真实库列(能抓到 schema 漂移)。"""
|
||||
print("\n[3] 模型字段与数据库列一致性")
|
||||
failures = []
|
||||
checked = 0
|
||||
# 引擎无关:用 Django 的 introspection API(SQLite/PG/MySQL 都支持)
|
||||
with connection.cursor() as cur:
|
||||
for app_label in OWN_APPS:
|
||||
try:
|
||||
config = django_apps.get_app_config(app_label)
|
||||
except LookupError:
|
||||
continue
|
||||
for model in config.get_models():
|
||||
table = model._meta.db_table
|
||||
try:
|
||||
desc = connection.introspection.get_table_description(cur, table)
|
||||
except Exception:
|
||||
failures.append(f"{app_label}.{model.__name__}: 表 {table} 不存在")
|
||||
continue
|
||||
cols = {c.name for c in desc}
|
||||
checked += 1
|
||||
for field in model._meta.concrete_fields:
|
||||
if field.column not in cols:
|
||||
failures.append(
|
||||
f"{app_label}.{model.__name__}: 缺列 {field.column}"
|
||||
)
|
||||
if failures:
|
||||
bad(f"{len(failures)} 处不一致:")
|
||||
for f in failures[:30]:
|
||||
print(" " + f)
|
||||
print("\n 修复:python manage.py makemigrations && python manage.py migrate")
|
||||
return 1
|
||||
ok(f"{checked} 个模型的字段与数据库一致")
|
||||
return 0
|
||||
|
||||
|
||||
def check_new_app_tables() -> int:
|
||||
"""4. 关键新表存在性(新 app 最容易漏迁移)。"""
|
||||
print("\n[4] 关键表存在性")
|
||||
required = [
|
||||
("ai", "AiUsage"),
|
||||
("billing", "Plan"),
|
||||
("billing", "Subscription"),
|
||||
("storefront", "StorefrontAccount"),
|
||||
("storefront", "CustomerProductAuth"),
|
||||
("storefront", "StorefrontOrder"),
|
||||
("storefront", "StorefrontOrderLine"),
|
||||
]
|
||||
missing = []
|
||||
for app_label, model_name in required:
|
||||
try:
|
||||
model = django_apps.get_model(app_label, model_name)
|
||||
except LookupError:
|
||||
missing.append(f"{app_label}.{model_name} 模型未注册")
|
||||
continue
|
||||
try:
|
||||
model.objects.exists()
|
||||
except Exception as exc:
|
||||
missing.append(f"{app_label}.{model_name}: {type(exc).__name__}")
|
||||
if missing:
|
||||
bad("以下关键表不可用:")
|
||||
for m in missing:
|
||||
print(" " + m)
|
||||
return 1
|
||||
ok(f"{len(required)} 张关键表可查")
|
||||
return 0
|
||||
|
||||
|
||||
def main() -> int:
|
||||
db = connection.settings_dict
|
||||
print(f"迁移健康检查 · 库={db.get('NAME')} · 引擎={db.get('ENGINE')}")
|
||||
failures = 0
|
||||
failures += check_pending_migrations()
|
||||
failures += check_unapplied_migrations()
|
||||
failures += check_model_columns()
|
||||
failures += check_new_app_tables()
|
||||
|
||||
print()
|
||||
if failures:
|
||||
print(f"{RED}检查未通过:{failures} 项问题{RESET}")
|
||||
return 1
|
||||
print(f"{GREEN}全部通过{RESET}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user