from datetime import UTC, datetime from sqlalchemy import Boolean, DateTime, ForeignKey, Index, Integer, String from sqlalchemy.ext.asyncio import AsyncAttrs from sqlalchemy.orm import DeclarativeBase, Mapped, declared_attr, mapped_column, relationship from app.utils.common_util import uuid4_str class MappedBase(AsyncAttrs, DeclarativeBase): """声明式基类 `AsyncAttrs `__ `DeclarativeBase `__ `mapped_column() `__ 兼容 SQLite、MySQL 和 PostgreSQL """ __abstract__: bool = True @declared_attr.directive def __tablename__(cls) -> str: return cls.__name__.lower() class ModelMixin(MappedBase): """模型混入类 - 提供通用字段和功能 基础模型混合类 Mixin: 一种面向对象编程概念, 使结构变得更加清晰 数据隔离设计原则: ================== 数据权限 (created_id/updated_id): - 配合角色的data_scope字段实现精细化权限控制 - 1:仅本人 - 2:本部门 - 3:本部门及以下 - 4:全部数据 - 5:自定义 SQLAlchemy加载策略说明: - select(默认): 延迟加载,访问时单独查询 - joined: 使用LEFT JOIN预加载 - selectin: 使用IN查询批量预加载(推荐用于一对多) - subquery: 使用子查询预加载 - raise/raise_on_sql: 禁止加载 - noload: 不加载,返回None - immediate: 立即加载 - write_only: 只写不读 - dynamic: 返回查询对象,支持进一步过滤 """ __abstract__: bool = True @declared_attr.directive def __table_args__(cls) -> tuple: table_name = cls.__tablename__ if hasattr(cls, '__tablename__') else cls.__name__.lower() return ( Index(f"ix_{table_name}_status_deleted", "status", "is_deleted"), Index(f"ix_{table_name}_created_deleted", "created_time", "is_deleted"), ) # 基础字段 id: Mapped[int] = mapped_column( Integer, primary_key=True, autoincrement=True, comment="主键ID", index=True, ) uuid: Mapped[str] = mapped_column( String(64), default=uuid4_str, nullable=False, unique=True, comment="UUID全局唯一标识", index=True, ) is_deleted: Mapped[bool] = mapped_column( Boolean, default=False, nullable=False, comment="是否已删除(0:未删除 1:已删除)", index=True, ) created_time: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=lambda: datetime.now(UTC), nullable=False, comment="创建时间", index=True, ) updated_time: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC), nullable=False, comment="更新时间", ) deleted_time: Mapped[datetime | None] = mapped_column( DateTime(timezone=True), default=None, nullable=True, comment="删除时间", ) class UserMixin(MappedBase): """用户审计字段 Mixin CRUD(base_crud.py)会自动检测并预加载 created_by/updated_by(使用 joinedload,一对一关系最高效), 无需在 service 层显式声明。deleted_by 仅在回收站等特定场景需要时通过 preload 参数显式获取。 """ __abstract__: bool = True created_id: Mapped[int | None] = mapped_column( Integer, ForeignKey("sys_user.id", ondelete="SET NULL", onupdate="CASCADE"), default=None, nullable=True, index=True, comment="创建人ID", ) updated_id: Mapped[int | None] = mapped_column( Integer, ForeignKey("sys_user.id", ondelete="SET NULL", onupdate="CASCADE"), default=None, nullable=True, index=True, comment="更新人ID", ) deleted_id: Mapped[int | None] = mapped_column( Integer, ForeignKey("sys_user.id", ondelete="SET NULL", onupdate="CASCADE"), default=None, nullable=True, index=True, comment="删除人ID", ) @declared_attr def created_by(self): """创建人关联关系""" return relationship( "UserModel", foreign_keys=lambda: self.created_id, # pyright: ignore[reportArgumentType] uselist=False, ) @declared_attr def updated_by(self): """更新人关联关系""" return relationship( "UserModel", foreign_keys=lambda: self.updated_id, # pyright: ignore[reportArgumentType] uselist=False, ) @declared_attr def deleted_by(self): """删除人关联关系""" return relationship( "UserModel", foreign_keys=lambda: self.deleted_id, # pyright: ignore[reportArgumentType] uselist=False, )