refactor: 统一将creator_id重命名为created_id并优化模型关系

feat: 新增系统租户和用户初始化数据

style: 优化代码注释和格式

fix: 修复数据权限检查逻辑中的created_id字段判断

perf: 移除不必要的模型关系以提升性能

docs: 更新初始化数据的描述信息

chore: 清理废弃文件和冗余代码
This commit is contained in:
zhangtao
2025-11-24 01:02:27 +08:00
parent 9a39a64faf
commit c0d32a7eec
59 changed files with 581 additions and 867 deletions
+19 -19
View File
@@ -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]:
+63 -14
View File
@@ -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
)
+27 -5
View File
@@ -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='新文件映射路径')
+1 -1
View File
@@ -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