You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

161 lines
5.9 KiB
Python

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

from datetime import datetime
from typing import TYPE_CHECKING, Any, TypeVar
from pydantic import BaseModel, ConfigDict, Field, model_validator
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.validator import DateTimeStr
if TYPE_CHECKING:
# 仅供 IDE 类型推断,运行时不会触发导入,避免 app.core ↔ app.api.v1 循环依赖
from app.api.v1.module_system.user.model import UserModel
UserT = TypeVar("UserT")
class CommonSchema(BaseModel):
"""通用信息模型"""
model_config = ConfigDict(from_attributes=True)
id: int = Field(description="编号ID")
name: str = Field(description="名称")
class BaseSchema(BaseModel):
"""通用输出模型,包含基础字段和审计字段"""
model_config = ConfigDict(from_attributes=True)
id: int | None = Field(default=None, description="主键ID")
uuid: str | None = Field(default=None, description="UUID")
created_time: DateTimeStr | None = Field(default=None, description="创建时间")
updated_time: DateTimeStr | None = Field(default=None, description="更新时间")
is_deleted: bool = Field(default=False, description="是否已删除")
deleted_time: DateTimeStr | None = Field(default=None, description="删除时间")
class UserBySchema(BaseModel):
"""通用创建模型,包含基础字段和审计字段"""
model_config = ConfigDict(from_attributes=True)
created_id: int | None = Field(default=None, description="创建人ID")
created_by: CommonSchema | None = Field(default=None, description="创建人信息")
updated_id: int | None = Field(default=None, description="更新人ID")
updated_by: CommonSchema | None = Field(default=None, description="更新人信息")
deleted_id: int | None = Field(default=None, description="删除人ID")
deleted_by: CommonSchema | None = Field(default=None, description="删除人信息")
class TenantBySchema(BaseModel):
"""租户嵌套出参(不再使用扁平 tenant_id / tenant_name / tenant_code"""
model_config = ConfigDict(from_attributes=True)
tenant_id: int | None = Field(default=None, description="租户ID")
tenant_by: CommonSchema | None = Field(default=None, description="租户信息")
class BatchSetAvailable(BaseModel):
"""批量设置可用状态的请求模型"""
ids: list[int] = Field(default_factory=list, description="ID列表")
status: int = Field(default=0, ge=0, le=1, description="是否可用")
class UploadResponseSchema(BaseModel):
"""上传响应模型"""
model_config = ConfigDict(from_attributes=True)
file_path: str | None = Field(default=None, description="新文件映射路径")
file_name: str | None = Field(default=None, description="新文件名称")
origin_name: str | None = Field(default=None, description="原文件名称")
file_url: str | None = Field(default=None, description="新文件访问地址")
class DownloadFileSchema(BaseModel):
"""下载文件模型"""
file_path: str = Field(..., description="新文件映射路径")
file_name: str = Field(..., description="新文件名称")
class BatchDelete(BaseModel):
"""批量删除请求模型"""
ids: list[int] = Field(..., min_length=1, description="ID列表")
class AuthSchema(BaseModel):
"""权限认证模型
``user`` 字段运行时为 ``Any``(避免与 SQLAlchemy 懒加载冲突,也避免
循环依赖)。通过 ``get_user()`` 方法获得 IDE 类型推断。
"""
model_config = ConfigDict(arbitrary_types_allowed=True)
user: Any = Field(default=None, description="用户信息UserModel 实例)", exclude=True)
check_data_scope: bool = Field(default=True, description="是否检查数据权限")
db: AsyncSession | None = Field(default=None, description="数据库会话", exclude=True)
tenant_id: int | None = Field(default=None, description="租户ID,用于用户认证前查询")
def get_user(self) -> "UserModel | None":
"""类型化的用户访问方法。
业务方 ``auth.get_user()`` 由 IDE 推断为 ``UserModel | None``
享受自动补全。``auth.user`` 仍可用但无类型补全。
"""
return self.user # type: ignore[return-value]
class JWTPayloadSchema(BaseModel):
"""JWT载荷模型"""
sub: str = Field(..., description="用户登录信息")
is_refresh: bool = Field(default=False, description="是否刷新token")
exp: datetime | int = Field(..., description="过期时间")
@model_validator(mode="after")
def validate_fields(self):
if not self.sub or len(self.sub.strip()) == 0:
raise ValueError("会话编号不能为空")
return self
class JWTOutSchema(BaseModel):
"""JWT响应模型"""
model_config = ConfigDict(from_attributes=True)
access_token: str = Field(..., min_length=1, description="访问token")
refresh_token: str = Field(..., min_length=1, description="刷新token")
token_type: str = Field(default="Bearer", description="token类型")
expires_in: int = Field(..., gt=0, description="过期时间(秒)")
class RefreshTokenPayloadSchema(BaseModel):
"""刷新Token载荷模型"""
refresh_token: str = Field(..., min_length=1, description="刷新token")
class LogoutPayloadSchema(BaseModel):
"""退出登录载荷模型"""
token: str = Field(..., min_length=1, description="token")
class PageResultSchema[T](BaseModel):
"""分页查询结果模型"""
model_config = ConfigDict(from_attributes=True)
page_no: int | None = Field(default=None, ge=1, description="页码默认为1")
page_size: int | None = Field(default=None, ge=1, description="页面大小默认为10")
total: int = Field(default=0, ge=0, description="总记录数")
has_next: bool | None = Field(default=False, description="是否有下一页")
items: list[T] = Field(default_factory=list, description="分页后的数据列表")