mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-21 04:46:26 +00:00
refactor: 统一将creator_id重命名为created_id并优化模型关系
feat: 新增系统租户和用户初始化数据 style: 优化代码注释和格式 fix: 修复数据权限检查逻辑中的created_id字段判断 perf: 移除不必要的模型关系以提升性能 docs: 更新初始化数据的描述信息 chore: 清理废弃文件和冗余代码
This commit is contained in:
@@ -209,8 +209,8 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
|
||||
obj = self.model(**obj_dict)
|
||||
|
||||
# 设置创建人ID(存在该字段时)
|
||||
if hasattr(obj, "creator_id") and self.current_user:
|
||||
setattr(obj, "creator_id", self.current_user.id)
|
||||
if hasattr(obj, "created_id") and self.current_user:
|
||||
setattr(obj, "created_id", self.current_user.id)
|
||||
|
||||
self.db.add(obj)
|
||||
await self.db.flush()
|
||||
@@ -335,8 +335,8 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
|
||||
if not self.current_user or not self.auth.check_data_scope:
|
||||
return None
|
||||
|
||||
# 如果模型没有创建人creator_id字段,则不限制
|
||||
if not hasattr(self.model, "creator_id"):
|
||||
# 如果模型没有创建人created_id字段,则不限制
|
||||
if not hasattr(self.model, "created_id"):
|
||||
return None
|
||||
|
||||
# 超级管理员可以查看所有数据
|
||||
@@ -345,9 +345,9 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
|
||||
|
||||
# 如果用户没有部门或角色,则只能查看自己的数据
|
||||
if not getattr(self.current_user, "dept_id", None) or not getattr(self.current_user, "roles", None):
|
||||
creator_id_attr = getattr(self.model, "creator_id", None)
|
||||
if creator_id_attr is not None:
|
||||
return creator_id_attr == self.current_user.id
|
||||
created_id_attr = getattr(self.model, "created_id", None)
|
||||
if created_id_attr is not None:
|
||||
return created_id_attr == self.current_user.id
|
||||
return None
|
||||
|
||||
# 获取用户所有角色的权限范围
|
||||
@@ -374,9 +374,9 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
|
||||
if hasattr(UserModel, 'dept_id') and creator_rel is not None:
|
||||
return creator_rel.has(getattr(UserModel, 'dept_id').in_(list(dept_ids)))
|
||||
else:
|
||||
creator_id_attr = getattr(self.model, "creator_id", None)
|
||||
if creator_id_attr is not None:
|
||||
return creator_id_attr == self.current_user.id
|
||||
created_id_attr = getattr(self.model, "created_id", None)
|
||||
if created_id_attr is not None:
|
||||
return created_id_attr == self.current_user.id
|
||||
return None
|
||||
|
||||
# 处理其他数据权限范围
|
||||
@@ -384,9 +384,9 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
|
||||
|
||||
if 1 in data_scopes:
|
||||
# 仅本人数据
|
||||
creator_id_attr = getattr(self.model, "creator_id", None)
|
||||
if creator_id_attr is not None:
|
||||
return creator_id_attr == self.current_user.id
|
||||
created_id_attr = getattr(self.model, "created_id", None)
|
||||
if created_id_attr is not None:
|
||||
return created_id_attr == self.current_user.id
|
||||
return None
|
||||
|
||||
if 2 in data_scopes and dept_id_val is not None:
|
||||
@@ -411,15 +411,15 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
|
||||
if hasattr(UserModel, 'dept_id') and creator_rel is not None and dept_ids:
|
||||
return creator_rel.has(getattr(UserModel, 'dept_id').in_(list(dept_ids)))
|
||||
else:
|
||||
creator_id_attr = getattr(self.model, "creator_id", None)
|
||||
if creator_id_attr is not None:
|
||||
return creator_id_attr == self.current_user.id
|
||||
created_id_attr = getattr(self.model, "created_id", None)
|
||||
if created_id_attr is not None:
|
||||
return created_id_attr == self.current_user.id
|
||||
return None
|
||||
|
||||
# 默认情况下,只能查看自己的数据
|
||||
creator_id_attr = getattr(self.model, "creator_id", None)
|
||||
if creator_id_attr is not None:
|
||||
return creator_id_attr == self.current_user.id
|
||||
created_id_attr = getattr(self.model, "created_id", None)
|
||||
if created_id_attr is not None:
|
||||
return created_id_attr == self.current_user.id
|
||||
return None
|
||||
|
||||
async def __build_conditions(self, **kwargs) -> List[ColumnElement]:
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from sqlalchemy import DateTime, String, Integer, Text, ForeignKey
|
||||
from sqlalchemy.orm import Mapped, mapped_column, DeclarativeBase
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column, DeclarativeBase, declared_attr, relationship
|
||||
from sqlalchemy.ext.asyncio import AsyncAttrs
|
||||
from typing import Optional
|
||||
|
||||
from app.utils.common_util import uuid4_str
|
||||
|
||||
|
||||
class MappedBase(AsyncAttrs, DeclarativeBase):
|
||||
@@ -52,18 +53,11 @@ class ModelMixin(MappedBase):
|
||||
- 4:全部数据 → WHERE tenant_id = current_tenant_id
|
||||
- 5:自定义 → WHERE dept_id IN (role_depts)
|
||||
|
||||
4. 软删除 (deleted_at):
|
||||
- NULL: 正常数据(未删除)
|
||||
- 时间戳: 已删除数据
|
||||
- 查询时默认过滤: WHERE deleted_at IS NULL
|
||||
- 优点: 数据可恢复,保留审计追踪
|
||||
- 注意: 需要在唯一索引中包含deleted_at字段
|
||||
|
||||
继承规则:
|
||||
- 需要租户隔离的业务表继承此类
|
||||
- 不需要隔离的表(如租户表本身)只继承MappedBase
|
||||
|
||||
SQLAlchemy加载策略说明:
|
||||
SQLAlchemy加载策略说明:
|
||||
- select(默认): 延迟加载,访问时单独查询
|
||||
- joined: 使用LEFT JOIN预加载
|
||||
- selectin: 使用IN查询批量预加载(推荐用于一对多)
|
||||
@@ -78,12 +72,11 @@ class ModelMixin(MappedBase):
|
||||
|
||||
# 基础字段
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True, comment='主键ID')
|
||||
uuid: Mapped[str] = mapped_column(UUID(as_uuid=True), default=uuid.uuid4, nullable=False, unique=True, comment='UUID全局唯一标识')
|
||||
uuid: Mapped[str] = mapped_column(String(64), default=uuid4_str, nullable=False, unique=True, comment='UUID全局唯一标识')
|
||||
status: Mapped[str] = mapped_column(String(10), default='0', nullable=False, comment="是否启用(0:启用 1:禁用)")
|
||||
description: Mapped[str | None] = mapped_column(Text, default=None, nullable=True, comment="备注/描述")
|
||||
created_time: Mapped[datetime] = mapped_column(DateTime, default=datetime.now, nullable=False, comment='创建时间')
|
||||
updated_time: Mapped[datetime] = mapped_column(DateTime, default=datetime.now, onupdate=datetime.now, nullable=False, comment='更新时间')
|
||||
deleted_at: Mapped[datetime | None] = mapped_column(DateTime, default=None, nullable=True, index=True, comment='软删除时间(NULL:未删除, 时间戳:已删除)')
|
||||
|
||||
|
||||
class UserMixin(MappedBase):
|
||||
@@ -119,6 +112,34 @@ class UserMixin(MappedBase):
|
||||
comment="更新人ID"
|
||||
)
|
||||
|
||||
@declared_attr
|
||||
def created_by(cls) -> Mapped["UserModel | None"]:
|
||||
"""
|
||||
创建人关联关系(延迟加载,避免循环依赖)
|
||||
"""
|
||||
return relationship(
|
||||
"UserModel",
|
||||
primaryjoin=f"{cls.__name__}.created_id == UserModel.id",
|
||||
lazy="selectin",
|
||||
foreign_keys=lambda: [cls.created_id],
|
||||
viewonly=True,
|
||||
uselist=False
|
||||
)
|
||||
|
||||
@declared_attr
|
||||
def updated_by(cls) -> Mapped[Optional["UserModel"]]:
|
||||
"""
|
||||
更新人关联关系(延迟加载,避免循环依赖)
|
||||
"""
|
||||
return relationship(
|
||||
"UserModel",
|
||||
primaryjoin=f"{cls.__name__}.updated_id == UserModel.id",
|
||||
lazy="selectin",
|
||||
foreign_keys=lambda: [cls.updated_id],
|
||||
viewonly=True,
|
||||
uselist=False
|
||||
)
|
||||
|
||||
|
||||
class TenantMixin(MappedBase):
|
||||
"""
|
||||
@@ -141,7 +162,7 @@ class TenantMixin(MappedBase):
|
||||
"""
|
||||
__abstract__: bool = True
|
||||
|
||||
tenant_id: Mapped[int] = mapped_column(
|
||||
tenant_id: Mapped[int | None] = mapped_column(
|
||||
Integer,
|
||||
ForeignKey("system_tenant.id", ondelete="CASCADE", onupdate="CASCADE"),
|
||||
nullable=False,
|
||||
@@ -149,6 +170,20 @@ class TenantMixin(MappedBase):
|
||||
comment="所属租户ID"
|
||||
)
|
||||
|
||||
@declared_attr
|
||||
def tenant(cls) -> Mapped["TenantModel"]:
|
||||
"""
|
||||
租户关联关系(延迟加载,避免循环依赖)
|
||||
"""
|
||||
return relationship(
|
||||
"TenantModel",
|
||||
primaryjoin=f"{cls.__name__}.tenant_id == TenantModel.id",
|
||||
lazy="selectin",
|
||||
foreign_keys=lambda: [cls.tenant_id],
|
||||
viewonly=True,
|
||||
uselist=False
|
||||
)
|
||||
|
||||
|
||||
class CustomerMixin(MappedBase):
|
||||
"""
|
||||
@@ -181,3 +216,17 @@ class CustomerMixin(MappedBase):
|
||||
index=True,
|
||||
comment="所属客户ID(NULL表示租户级数据,>0表示客户级数据)"
|
||||
)
|
||||
|
||||
@declared_attr
|
||||
def customer(cls) -> Mapped["CustomerModel | None"]:
|
||||
"""
|
||||
客户关联关系(延迟加载,避免循环依赖)
|
||||
"""
|
||||
return relationship(
|
||||
"CustomerModel",
|
||||
primaryjoin=f"{cls.__name__}.customer_id == CustomerModel.id",
|
||||
lazy="selectin",
|
||||
foreign_keys=lambda: [cls.customer_id],
|
||||
viewonly=True,
|
||||
uselist=False
|
||||
)
|
||||
|
||||
@@ -14,8 +14,9 @@ class UserInfoSchema(BaseModel):
|
||||
name: str = Field(description="用户姓名")
|
||||
username: str = Field(description="用户名")
|
||||
|
||||
|
||||
class CommonSchema(BaseModel):
|
||||
"""用户信息模型"""
|
||||
"""通用信息模型"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int = Field(description="编号ID")
|
||||
@@ -27,11 +28,31 @@ class BaseSchema(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: Optional[int] = Field(default=None, description="主键ID")
|
||||
uuid: Optional[str] = Field(default=None, description="UUID")
|
||||
status: Optional[bool] = Field(default=None, description="状态")
|
||||
description: Optional[str] = Field(default=None, description="描述")
|
||||
created_at: Optional[DateTimeStr] = Field(default=None, description="创建时间")
|
||||
updated_at: Optional[DateTimeStr] = Field(default=None, description="更新时间")
|
||||
creator_id: Optional[int] = Field(default=None, description="创建人ID")
|
||||
creator: Optional[UserInfoSchema] = Field(default=None, description="创建人信息")
|
||||
created_time: Optional[DateTimeStr] = Field(default=None, description="创建时间")
|
||||
updated_time: Optional[DateTimeStr] = Field(default=None, description="更新时间")
|
||||
created_id: Optional[int] = Field(default=None, description="创建人ID")
|
||||
created_by: Optional[UserInfoSchema] = Field(default=None, description="创建人信息")
|
||||
updated_id: Optional[int] = Field(default=None, description="更新人ID")
|
||||
updated_by: Optional[UserInfoSchema] = Field(default=None, description="更新人信息")
|
||||
|
||||
|
||||
class TenantSchema(BaseSchema):
|
||||
"""租户模型"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
tenant_id: Optional[int] = Field(default=None, description="所属租户ID")
|
||||
tenant: Optional[CommonSchema] = Field(default=None, description="租户信息")
|
||||
|
||||
|
||||
class CustomerSchema(BaseSchema):
|
||||
"""客户模型"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
customer_id: Optional[int] = Field(default=None, description="所属客户ID")
|
||||
customer: Optional[CommonSchema] = Field(default=None, description="客户信息")
|
||||
|
||||
|
||||
class BatchSetAvailable(BaseModel):
|
||||
@@ -49,6 +70,7 @@ class UploadResponseSchema(BaseModel):
|
||||
origin_name: Optional[str] = Field(default=None, description='原文件名称')
|
||||
file_url: Optional[str] = Field(default=None, description='新文件访问地址')
|
||||
|
||||
|
||||
class DownloadFileSchema(BaseModel):
|
||||
"""下载文件模型"""
|
||||
file_path: str = Field(..., description='新文件映射路径')
|
||||
|
||||
@@ -138,7 +138,7 @@ class OperationLogRoute(APIRoute):
|
||||
response_json = response_data.decode() if isinstance(response_data, (bytes, bytearray)) else str(response_data),
|
||||
process_time = process_time,
|
||||
description = route.summary,
|
||||
creator_id = current_user_id
|
||||
created_id = current_user_id
|
||||
), auth = auth)
|
||||
|
||||
return response
|
||||
|
||||
Reference in New Issue
Block a user