"""ORM 基类与公共 Mixin。 约定: * 主键统一 id 自增 * 所有表都带 created_at / updated_at * 业务表一律逻辑删除:is_del = 0 表示正常,1 表示已删除 * 查询正常数据统一写 `Model.alive()`,禁止在 DAO 里手写 `is_del == 1` (上一版项目就是把方向写反导致了"查不到刚建的数据") """ from __future__ import annotations from datetime import datetime from typing import Any from sqlalchemy import DateTime, SmallInteger from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column DEL_FLAG_NORMAL: int = 0 DEL_FLAG_DELETED: int = 1 class Base(DeclarativeBase): """SQLAlchemy 2.0 DeclarativeBase。""" def to_dict(self, exclude: set[str] | None = None) -> dict[str, Any]: exclude = exclude or set() return { c.name: getattr(self, c.name) for c in self.__table__.columns if c.name not in exclude } def __repr__(self) -> str: # pragma: no cover - 调试用 pk = getattr(self, "id", None) return f"<{self.__class__.__name__} id={pk}>" class TimestampMixin: created_at: Mapped[datetime] = mapped_column( DateTime, default=datetime.now, nullable=False, comment="创建时间" ) updated_at: Mapped[datetime] = mapped_column( DateTime, default=datetime.now, onupdate=datetime.now, nullable=False, comment="更新时间" ) class SoftDeleteMixin: is_del: Mapped[int] = mapped_column( SmallInteger, default=DEL_FLAG_NORMAL, server_default="0", nullable=False, index=True, comment="逻辑删除:0=正常 1=已删除", ) @classmethod def alive(cls): """正常数据的过滤条件,供 select().where() 使用。""" return cls.is_del == DEL_FLAG_NORMAL def soft_delete(self) -> None: self.is_del = DEL_FLAG_DELETED