mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-21 20:55:14 +00:00
refactor: 大规模代码整理与功能优化
1. 重构后端API路由、CRUD与模块结构,整合日志管理,移除废弃demo代码 2. 优化前端组件类型定义、样式与路由配置,修复权限判断逻辑 3. 调整默认排序规则、滚动条样式与工具类函数,更新依赖与配置文件 4. 修复多处类型不匹配与默认值问题,完善表单与菜单验证逻辑
This commit is contained in:
+239
-27
@@ -39,7 +39,7 @@ class TestModels:
|
||||
def test_tenant_model(self) -> None:
|
||||
from app.api.v1.module_platform.tenant.model import TenantModel
|
||||
|
||||
assert TenantModel.__tablename__ == "sys_tenant"
|
||||
assert TenantModel.__tablename__ == "platform_tenant"
|
||||
assert hasattr(TenantModel, "name")
|
||||
assert hasattr(TenantModel, "code")
|
||||
assert hasattr(TenantModel, "status")
|
||||
@@ -49,27 +49,30 @@ class TestModels:
|
||||
"""测试租户模型中的配额字段(单一大表设计)"""
|
||||
from app.api.v1.module_platform.tenant.model import TenantModel
|
||||
|
||||
assert hasattr(TenantModel, "max_users")
|
||||
assert hasattr(TenantModel, "max_roles")
|
||||
assert hasattr(TenantModel, "max_storage_mb")
|
||||
assert hasattr(TenantModel, "max_depts")
|
||||
assert hasattr(TenantModel, "package_id")
|
||||
assert hasattr(TenantModel, "start_time")
|
||||
|
||||
def test_tenant_config_model(self) -> None:
|
||||
from app.api.v1.module_platform.tenant.model import TenantConfigModel
|
||||
"""TenantModel 包含配置字段(单一大表设计)"""
|
||||
from app.api.v1.module_platform.tenant.model import TenantModel
|
||||
|
||||
assert TenantConfigModel.__tablename__ == "sys_tenant_config"
|
||||
assert hasattr(TenantConfigModel, "config_key")
|
||||
# 配置字段已合并到 TenantModel
|
||||
assert hasattr(TenantModel, "name")
|
||||
assert hasattr(TenantModel, "code")
|
||||
assert hasattr(TenantModel, "status")
|
||||
|
||||
def test_tenant_menu_model(self) -> None:
|
||||
from app.api.v1.module_platform.tenant.model import TenantMenuModel
|
||||
def test_package_model_quota(self) -> None:
|
||||
from app.api.v1.module_platform.package.model import PackageModel
|
||||
|
||||
assert TenantMenuModel.__tablename__ == "sys_tenant_menu"
|
||||
assert hasattr(TenantMenuModel, "menu_id")
|
||||
assert hasattr(PackageModel, "max_users")
|
||||
assert hasattr(PackageModel, "max_roles")
|
||||
assert hasattr(PackageModel, "max_depts")
|
||||
assert hasattr(PackageModel, "max_storage_mb")
|
||||
|
||||
def test_tenant_user_model(self) -> None:
|
||||
from app.api.v1.module_platform.tenant.model import TenantUserModel
|
||||
|
||||
assert TenantUserModel.__tablename__ == "sys_user_tenant"
|
||||
assert TenantUserModel.__tablename__ == "platform_user_tenant"
|
||||
assert hasattr(TenantUserModel, "role")
|
||||
assert hasattr(TenantUserModel, "is_default")
|
||||
|
||||
@@ -86,7 +89,7 @@ class TestModels:
|
||||
assert hasattr(RoleModel, "menus")
|
||||
|
||||
def test_menu_model_has_type(self) -> None:
|
||||
from app.api.v1.module_system.menu.model import MenuModel
|
||||
from app.api.v1.module_platform.menu.model import MenuModel
|
||||
|
||||
assert hasattr(MenuModel, "type")
|
||||
assert hasattr(MenuModel, "name")
|
||||
@@ -102,16 +105,16 @@ class TestModels:
|
||||
def test_plugin_model(self) -> None:
|
||||
from app.api.v1.module_platform.plugin.model import PluginModel
|
||||
|
||||
assert PluginModel.__tablename__ == "sys_plugin"
|
||||
assert PluginModel.__tablename__ == "platform_plugin"
|
||||
assert hasattr(PluginModel, "code")
|
||||
|
||||
def test_tenant_plugin_model(self) -> None:
|
||||
from app.api.v1.module_platform.plugin.model import TenantPluginModel
|
||||
|
||||
assert TenantPluginModel.__tablename__ == "sys_tenant_plugin"
|
||||
assert TenantPluginModel.__tablename__ == "platform_tenant_plugin"
|
||||
|
||||
def test_ticket_model(self) -> None:
|
||||
from app.api.v1.module_platform.ticket.model import TicketModel
|
||||
from app.api.v1.module_system.ticket.model import TicketModel
|
||||
|
||||
assert TicketModel.__tablename__ == "sys_ticket"
|
||||
assert hasattr(TicketModel, "ticket_type")
|
||||
@@ -127,7 +130,7 @@ class TestSchemas:
|
||||
def test_tenant_create_schema(self) -> None:
|
||||
from app.api.v1.module_platform.tenant.schema import TenantCreateSchema
|
||||
|
||||
s = TenantCreateSchema(name="test", code="test001", status="0")
|
||||
s = TenantCreateSchema(name="test", code="test001", status=0)
|
||||
assert s.name == "test"
|
||||
assert s.code == "test001"
|
||||
|
||||
@@ -138,18 +141,19 @@ class TestSchemas:
|
||||
assert s.name == "renamed"
|
||||
assert s.code is None # 未传则为 None
|
||||
|
||||
def test_tenant_quota_schema_validation(self) -> None:
|
||||
from app.api.v1.module_platform.tenant.schema import TenantQuotaUpdateSchema
|
||||
def test_package_schema_quota(self) -> None:
|
||||
from app.api.v1.module_platform.package.schema import PackageCreateSchema
|
||||
|
||||
s = TenantQuotaUpdateSchema(max_users=100, max_roles=30)
|
||||
s = PackageCreateSchema(name="test", code="test", max_users=100, max_roles=30)
|
||||
assert s.max_users == 100
|
||||
assert s.max_roles == 30
|
||||
|
||||
# 负数应被拒绝
|
||||
with pytest.raises(Exception):
|
||||
TenantQuotaUpdateSchema(max_users=-1)
|
||||
with pytest.raises(ValueError):
|
||||
PackageCreateSchema(name="test", code="test", max_users=-1)
|
||||
|
||||
def test_ticket_schema(self) -> None:
|
||||
from app.api.v1.module_platform.ticket.schema import TicketCreateSchema
|
||||
from app.api.v1.module_system.ticket.schema import TicketCreateSchema
|
||||
|
||||
s = TicketCreateSchema(title="test", ticket_type="suggestion")
|
||||
assert s.title == "test"
|
||||
@@ -157,7 +161,7 @@ class TestSchemas:
|
||||
|
||||
def test_menu_type_enum(self) -> None:
|
||||
"""菜单类型: 1=目录 2=菜单 3=按钮 4=链接"""
|
||||
from app.api.v1.module_system.menu.model import MenuModel
|
||||
from app.api.v1.module_platform.menu.model import MenuModel
|
||||
|
||||
assert hasattr(MenuModel, "type")
|
||||
# type 是 int 字段
|
||||
@@ -203,9 +207,217 @@ class TestEnums:
|
||||
|
||||
def test_menu_type_values(self) -> None:
|
||||
"""type: 1=目录 2=菜单 3=按钮 4=链接"""
|
||||
from app.api.v1.module_system.menu.model import MenuModel
|
||||
from app.api.v1.module_platform.menu.model import MenuModel
|
||||
|
||||
assert MenuModel.__tablename__ == "sys_menu"
|
||||
assert MenuModel.__tablename__ == "platform_menu"
|
||||
# type 字段是 int 类型
|
||||
col = next((c for c in MenuModel.__table__.columns if c.name == "type"), None)
|
||||
assert col is not None
|
||||
|
||||
|
||||
# ==================== 06. 租户自助注册(PRD §4.5)====================
|
||||
|
||||
|
||||
class TestTenantRegister:
|
||||
"""TenantRegisterSchema / TenantRegisterOutSchema 校验。"""
|
||||
|
||||
def test_register_schema_valid(self) -> None:
|
||||
from app.api.v1.module_system.auth.schema import TenantRegisterSchema
|
||||
|
||||
s = TenantRegisterSchema(
|
||||
username="testowner",
|
||||
password="TestPass123",
|
||||
email="test@example.com",
|
||||
tenant_name="测试企业",
|
||||
)
|
||||
assert s.username == "testowner"
|
||||
assert s.email == "test@example.com"
|
||||
assert s.tenant_name == "测试企业"
|
||||
|
||||
def test_register_schema_minimal(self) -> None:
|
||||
"""仅必填字段。"""
|
||||
from app.api.v1.module_system.auth.schema import TenantRegisterSchema
|
||||
|
||||
s = TenantRegisterSchema(
|
||||
username="owner2",
|
||||
password="secret123",
|
||||
email="owner2@example.com",
|
||||
)
|
||||
assert s.tenant_name is None
|
||||
|
||||
def test_register_schema_short_password(self) -> None:
|
||||
"""密码少于 6 位应拒绝。"""
|
||||
from pydantic import ValidationError
|
||||
|
||||
from app.api.v1.module_system.auth.schema import TenantRegisterSchema
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
TenantRegisterSchema(
|
||||
username="x",
|
||||
password="123",
|
||||
email="a@b.com",
|
||||
)
|
||||
|
||||
def test_register_schema_bad_email(self) -> None:
|
||||
"""非法邮箱应拒绝。"""
|
||||
from pydantic import ValidationError
|
||||
|
||||
from app.api.v1.module_system.auth.schema import TenantRegisterSchema
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
TenantRegisterSchema(
|
||||
username="x",
|
||||
password="123456",
|
||||
email="not-an-email",
|
||||
)
|
||||
|
||||
def test_register_out_schema_fields(self) -> None:
|
||||
"""注册响应 Schema 包含所有必要字段。"""
|
||||
from app.api.v1.module_system.auth.schema import TenantRegisterOutSchema
|
||||
|
||||
fields = set(TenantRegisterOutSchema.model_fields.keys())
|
||||
for f in ("user_id", "tenant_id", "tenant_name", "tenant_code", "package", "trial_end", "message"):
|
||||
assert f in fields, f"{f} 未在 TenantRegisterOutSchema 中找到"
|
||||
|
||||
|
||||
# ==================== 07. 支付回调套餐激活 ====================
|
||||
|
||||
|
||||
class TestActivateTenantPackage:
|
||||
"""PaymentService._activate_tenant_package 日期计算逻辑。"""
|
||||
|
||||
def test_new_calculation(self) -> None:
|
||||
"""order_type=new:设置 start_time=now, end_time=now+period。"""
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
now = datetime(2025, 1, 1, 12, 0, 0)
|
||||
period_months = 1
|
||||
duration = timedelta(days=30 * period_months)
|
||||
|
||||
# 模拟 _activate_tenant_package 中 new 分支的日期计算
|
||||
fake = {"package_id": None, "start_time": None, "end_time": None, "status": 1}
|
||||
fake["package_id"] = 1
|
||||
fake["start_time"] = now
|
||||
fake["end_time"] = now + duration
|
||||
fake["status"] = 0
|
||||
|
||||
assert fake["package_id"] == 1
|
||||
assert fake["start_time"] == now
|
||||
assert fake["end_time"] == now + timedelta(days=30)
|
||||
assert fake["status"] == 0
|
||||
|
||||
def test_renew_extends_end_time(self) -> None:
|
||||
"""order_type=renew:在原有 end_time 基础上顺延。"""
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
now = datetime(2025, 1, 1)
|
||||
original_end = now + timedelta(days=30)
|
||||
fake = {"end_time": original_end, "status": 1}
|
||||
|
||||
# 模拟 renew 分支:base = max(end_time, now) + duration
|
||||
base = max(fake["end_time"], now)
|
||||
fake["end_time"] = base + timedelta(days=30)
|
||||
fake["status"] = 0
|
||||
|
||||
assert fake["end_time"] == original_end + timedelta(days=30)
|
||||
assert fake["status"] == 0
|
||||
|
||||
def test_upgrade_changes_package(self) -> None:
|
||||
"""order_type=upgrade:更换 package_id,状态置为 active。"""
|
||||
fake = {"package_id": 1, "status": 1}
|
||||
|
||||
fake["package_id"] = 2
|
||||
fake["status"] = 0
|
||||
|
||||
assert fake["package_id"] == 2
|
||||
assert fake["status"] == 0
|
||||
|
||||
|
||||
# ==================== 08. 插件热重载 ====================
|
||||
|
||||
|
||||
class TestPluginReload:
|
||||
"""discover.reload_dynamic_router 可正常导入且签名正确。"""
|
||||
|
||||
def test_reload_function_exists(self) -> None:
|
||||
from app.core.discover import reload_dynamic_router
|
||||
|
||||
assert callable(reload_dynamic_router)
|
||||
|
||||
def test_set_app_ref_exists(self) -> None:
|
||||
from app.core.discover import set_app_ref
|
||||
|
||||
assert callable(set_app_ref)
|
||||
|
||||
def test_reload_service_exists(self) -> None:
|
||||
"""PluginService.reload_service 方法存在且为类方法。"""
|
||||
import inspect
|
||||
|
||||
from app.api.v1.module_platform.plugin.service import PluginService
|
||||
|
||||
assert hasattr(PluginService, "reload_service")
|
||||
assert isinstance(
|
||||
inspect.getattr_static(PluginService, "reload_service"),
|
||||
(classmethod, classmethod),
|
||||
)
|
||||
|
||||
|
||||
# ==================== 18. status 字段类型一致性 ====================
|
||||
|
||||
class TestStatusFieldType:
|
||||
"""验证 status 字段从 String(10) 迁移为 SmallInteger 后的一致性"""
|
||||
|
||||
def test_model_mixin_status_is_integer(self) -> None:
|
||||
"""验证 ModelMixin 子类的 status 字段已转为 SmallInteger"""
|
||||
from app.api.v1.module_system.user.model import UserModel
|
||||
|
||||
col = UserModel.__table__.c["status"]
|
||||
assert "SMALLINT" in str(col.type).upper(), (
|
||||
f"UserModel.status 应为 SmallInteger,当前为 {col.type}"
|
||||
)
|
||||
|
||||
def test_tenant_status_is_integer(self) -> None:
|
||||
from app.api.v1.module_platform.tenant.model import TenantModel
|
||||
|
||||
col = TenantModel.__table__.c["status"]
|
||||
assert "SMALLINT" in str(col.type).upper(), (
|
||||
f"TenantModel.status 应为 SmallInteger,当前为 {col.type}"
|
||||
)
|
||||
|
||||
def test_user_status_is_integer(self) -> None:
|
||||
from app.api.v1.module_system.user.model import UserModel
|
||||
|
||||
col = UserModel.__table__.c["status"]
|
||||
assert "SMALLINT" in str(col.type).upper()
|
||||
|
||||
def test_role_status_is_integer(self) -> None:
|
||||
from app.api.v1.module_system.role.model import RoleModel
|
||||
|
||||
col = RoleModel.__table__.c["status"]
|
||||
assert "SMALLINT" in str(col.type).upper()
|
||||
|
||||
def test_dept_status_is_integer(self) -> None:
|
||||
from app.api.v1.module_system.dept.model import DeptModel
|
||||
|
||||
col = DeptModel.__table__.c["status"]
|
||||
assert "SMALLINT" in str(col.type).upper()
|
||||
|
||||
def test_menu_status_is_integer(self) -> None:
|
||||
from app.api.v1.module_platform.menu.model import MenuModel
|
||||
|
||||
col = MenuModel.__table__.c["status"]
|
||||
assert "SMALLINT" in str(col.type).upper()
|
||||
|
||||
def test_package_status_is_integer(self) -> None:
|
||||
from app.api.v1.module_platform.package.model import PackageModel
|
||||
|
||||
col = PackageModel.__table__.c["status"]
|
||||
assert "SMALLINT" in str(col.type).upper(), (
|
||||
f"PackageModel.status 应为 SmallInteger,当前为 {col.type}"
|
||||
)
|
||||
|
||||
def test_ticket_status_is_integer(self) -> None:
|
||||
from app.api.v1.module_system.ticket.model import TicketModel
|
||||
|
||||
col = TicketModel.__table__.c["status"]
|
||||
assert "SMALLINT" in str(col.type).upper()
|
||||
|
||||
Reference in New Issue
Block a user