baseline: 批次A-D 成果 + membership 半成品(测试红)

This commit is contained in:
agent
2026-09-11 23:11:35 +08:00
commit b3f3095d53
311 changed files with 40540 additions and 0 deletions
+190
View File
@@ -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())