"""Pydantic 公共模型与校验小工具。""" from __future__ import annotations from datetime import date, datetime from enum import Enum from typing import Any, Generic, TypeVar from pydantic import BaseModel, ConfigDict, Field, field_validator from app.model.constants import gender_to_code T = TypeVar("T") class ORMBase(BaseModel): """所有出参模型的基类:允许直接吃 ORM 对象(含 @property 派生字段)。""" model_config = ConfigDict(from_attributes=True, populate_by_name=True) class PageResult(BaseModel, Generic[T]): total: int = Field(0, description="总条数") page: int = Field(1, description="当前页") page_size: int = Field(10, description="每页条数") pages: int = Field(0, description="总页数") items: list[T] = Field(default_factory=list, description="数据列表") class SortOrder(str, Enum): ASC = "asc" DESC = "desc" class DateRangeMixin(BaseModel): start_date: str | None = Field(None, description="起始日期 YYYY-MM-DD") end_date: str | None = Field(None, description="结束日期 YYYY-MM-DD") # ---------------------------------------------------------------- 校验器 def _normalize_gender(cls, v): # noqa: ANN001, ANN201 if v is None: return v code = gender_to_code(v) if code is None: raise ValueError("性别只能是 1/2 或 男/女") return code def _strip_str(cls, v): # noqa: ANN001, ANN201 return v.strip() if isinstance(v, str) else v GENDER_VALIDATOR = field_validator("gender", mode="before", check_fields=False)(_normalize_gender) STRIP_VALIDATOR = field_validator("*", mode="before", check_fields=False)(_strip_str) def date_to_str(value: date | datetime | None) -> str | None: if value is None: return None if isinstance(value, datetime): return value.date().isoformat() return value.isoformat() def as_dict(model: Any) -> dict: return model.model_dump(exclude_unset=True)