feat(admin, miniapp): 优化数据库迁移逻辑与小程序图片资源处理
- 为后端Admin新增migration_compat工具,支持离线迁移时的基线资源检查,重构全部alembic迁移脚本适配离线模式,避免重复创建已存在的数据库表和字段 - 完善初始数据库迁移脚本,添加默认部门种子数据,新增ConciergeAdvisor详情字段从JSON到JSONB的类型转换迁移 - 小程序端新增图片资源解析工具resolveNetworkImage,实现本地静态资源到CDN的自动映射,添加图片加载容错降级机制 - 完善HeroSlide类型定义,新增actionLabel字段,优化首页轮播组件的文本渲染与错误处理逻辑 - 新增对应测试用例验证迁移路径正确性与前端数据处理逻辑 - 新增临时 brainstorm UI对比草稿与开发临时状态文件
This commit is contained in:
215
WonderQ-Admin/tests/test_clean_database_migration_path.py
Normal file
215
WonderQ-Admin/tests/test_clean_database_migration_path.py
Normal file
@@ -0,0 +1,215 @@
|
||||
from importlib.util import module_from_spec, spec_from_file_location
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
VERSIONS_DIR = Path(__file__).parents[1] / "alembic" / "versions"
|
||||
|
||||
|
||||
def load_migration(filename: str):
|
||||
path = VERSIONS_DIR / filename
|
||||
spec = spec_from_file_location(f"clean_database_{path.stem}", path)
|
||||
assert spec and spec.loader
|
||||
migration = module_from_spec(spec)
|
||||
spec.loader.exec_module(migration)
|
||||
return migration
|
||||
|
||||
|
||||
class ExistingRows:
|
||||
def first(self):
|
||||
return ("existing",)
|
||||
|
||||
|
||||
class EmptyRows:
|
||||
def first(self):
|
||||
return None
|
||||
|
||||
|
||||
class ExistingBind:
|
||||
def execute(self, _statement):
|
||||
return ExistingRows()
|
||||
|
||||
|
||||
class EmptyBind:
|
||||
def execute(self, _statement):
|
||||
return EmptyRows()
|
||||
|
||||
|
||||
class CurrentSchemaInspector:
|
||||
tables = {
|
||||
"AdminDepartment",
|
||||
"AdminMenu",
|
||||
"AdminRole",
|
||||
"AdminRoleDepartment",
|
||||
"AdminRoleMenu",
|
||||
"AdminUser",
|
||||
"AdminUserDepartment",
|
||||
"AdminUserRole",
|
||||
"AuditLog",
|
||||
"ConciergeAdvisor",
|
||||
"Customer",
|
||||
"DetailRecord",
|
||||
"HeroSlide",
|
||||
"HomeTeamBuilding",
|
||||
"HomeWanfaRecommendation",
|
||||
"HomeWildArchive",
|
||||
"Lead",
|
||||
"LeadFollowup",
|
||||
"MediaAsset",
|
||||
"Order",
|
||||
"VehicleOption",
|
||||
"WanfaCategory",
|
||||
"WanfaRoute",
|
||||
}
|
||||
|
||||
def get_table_names(self):
|
||||
return sorted(self.tables)
|
||||
|
||||
def get_columns(self, table_name):
|
||||
if table_name not in self.tables:
|
||||
return []
|
||||
return [{"name": "deptId"}, {"name": "createdById"}]
|
||||
|
||||
def get_indexes(self, table_name):
|
||||
if table_name not in self.tables:
|
||||
return []
|
||||
return [
|
||||
{"name": f"ix_{table_name}_deptId"},
|
||||
{"name": f"ix_{table_name}_createdById"},
|
||||
]
|
||||
|
||||
def get_foreign_keys(self, table_name):
|
||||
if table_name not in self.tables:
|
||||
return []
|
||||
return [
|
||||
{
|
||||
"name": f"fk_{table_name}_deptId",
|
||||
"constrained_columns": ["deptId"],
|
||||
"referred_table": "AdminDepartment",
|
||||
"referred_columns": ["id"],
|
||||
},
|
||||
{
|
||||
"name": f"fk_{table_name}_createdById",
|
||||
"constrained_columns": ["createdById"],
|
||||
"referred_table": "AdminUser",
|
||||
"referred_columns": ["id"],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def test_admin_rbac_does_not_recreate_tables_already_created_by_initial_baseline(monkeypatch):
|
||||
migration = load_migration("0025_admin_rbac.py")
|
||||
inspector = CurrentSchemaInspector()
|
||||
calls = []
|
||||
|
||||
monkeypatch.setattr(migration, "is_offline_mode", lambda: False, raising=False)
|
||||
monkeypatch.setattr(migration, "inspect", lambda _bind: inspector, raising=False)
|
||||
monkeypatch.setattr(migration.op, "get_bind", lambda: ExistingBind())
|
||||
monkeypatch.setattr(
|
||||
migration.op,
|
||||
"create_table",
|
||||
lambda *args, **_kwargs: calls.append(("create_table", args[0])),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
migration.op,
|
||||
"bulk_insert",
|
||||
lambda *args, **_kwargs: calls.append(("bulk_insert", args[0].name)),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
migration.op,
|
||||
"execute",
|
||||
lambda *_args, **_kwargs: calls.append(("execute",)),
|
||||
)
|
||||
|
||||
migration.upgrade()
|
||||
|
||||
assert [call for call in calls if call[0] in {"create_table", "bulk_insert"}] == []
|
||||
|
||||
|
||||
def test_initial_baseline_seeds_default_department_before_owned_data(monkeypatch):
|
||||
migration = load_migration("0001_initial_schema.py")
|
||||
inspector = CurrentSchemaInspector()
|
||||
calls = []
|
||||
|
||||
monkeypatch.setattr(migration.context, "is_offline_mode", lambda: False)
|
||||
monkeypatch.setattr(migration, "inspect", lambda _bind: inspector, raising=False)
|
||||
monkeypatch.setattr(migration.op, "get_bind", lambda: EmptyBind())
|
||||
monkeypatch.setattr(
|
||||
migration.op,
|
||||
"bulk_insert",
|
||||
lambda table, rows: calls.append((table.name, rows)),
|
||||
)
|
||||
|
||||
migration.upgrade()
|
||||
|
||||
assert [table_name for table_name, _rows in calls] == ["AdminDepartment"]
|
||||
assert calls[0][1][0]["id"] == migration.DEFAULT_DEPT_ID
|
||||
|
||||
|
||||
def test_concierge_details_migration_converts_json_to_jsonb(monkeypatch):
|
||||
migration_path = VERSIONS_DIR / "0035_concierge_details_jsonb.py"
|
||||
if not migration_path.exists():
|
||||
pytest.fail("0035_concierge_details_jsonb.py is required to align the model type")
|
||||
migration = load_migration("0035_concierge_details_jsonb.py")
|
||||
calls = []
|
||||
|
||||
monkeypatch.setattr(
|
||||
migration.op,
|
||||
"alter_column",
|
||||
lambda *args, **kwargs: calls.append((args, kwargs)),
|
||||
)
|
||||
|
||||
migration.upgrade()
|
||||
|
||||
assert migration.down_revision == "0034_media_assets"
|
||||
assert calls[0][0][:2] == ("ConciergeAdvisor", "details")
|
||||
assert calls[0][1]["postgresql_using"] == '"details"::jsonb'
|
||||
|
||||
|
||||
def test_offline_upgrade_generates_sql_for_a_clean_database():
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "alembic", "upgrade", "head", "--sql"],
|
||||
cwd=VERSIONS_DIR.parents[1],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
assert result.returncode == 0, result.stderr[-4000:]
|
||||
assert "0035_concierge_details_jsonb" in result.stdout
|
||||
assert result.stdout.count('CREATE TABLE "AdminRole"') == 1
|
||||
assert result.stdout.count('CREATE TABLE "MediaAsset"') == 1
|
||||
assert 'ALTER TABLE "HomeWildArchive" ADD COLUMN images' not in result.stdout
|
||||
assert 'ALTER TABLE "HomeTeamBuilding" ADD COLUMN "detailSubtitle"' not in result.stdout
|
||||
assert 'ALTER TABLE "DetailRecord" ADD COLUMN "conciergeAdvisorId"' not in result.stdout
|
||||
assert 'ALTER TABLE "Lead" ADD COLUMN "leadType"' not in result.stdout
|
||||
|
||||
|
||||
def test_admin_ownership_skips_missing_and_already_owned_tables(monkeypatch):
|
||||
migration = load_migration("0026_admin_ownership.py")
|
||||
inspector = CurrentSchemaInspector()
|
||||
calls = []
|
||||
|
||||
monkeypatch.setattr(migration, "is_offline_mode", lambda: False, raising=False)
|
||||
monkeypatch.setattr(migration, "inspect", lambda _bind: inspector, raising=False)
|
||||
monkeypatch.setattr(migration.op, "get_bind", lambda: ExistingBind())
|
||||
for operation in (
|
||||
"add_column",
|
||||
"create_index",
|
||||
"create_foreign_key",
|
||||
"execute",
|
||||
"alter_column",
|
||||
):
|
||||
monkeypatch.setattr(
|
||||
migration.op,
|
||||
operation,
|
||||
lambda *args, _operation=operation, **_kwargs: calls.append(
|
||||
(_operation, args[0] if args else None)
|
||||
),
|
||||
)
|
||||
|
||||
migration.upgrade()
|
||||
|
||||
assert calls == []
|
||||
Reference in New Issue
Block a user