from pydantic import BaseModel, Field, field_validator from datetime import datetime from typing import Optional, List from app.schemas.category import CategoryResponse from app.schemas.tag import TagResponse def parse_datetime(value): """解析日期时间字符串""" if value is None or value == '': return None if isinstance(value, datetime): return value # 尝试多种格式 formats = [ '%Y-%m-%dT%H:%M:%S', '%Y-%m-%d %H:%M:%S', '%Y-%m-%dT%H:%M:%S.%f', ] for fmt in formats: try: return datetime.strptime(value, fmt) except ValueError: continue # 最后尝试 ISO 格式 return datetime.fromisoformat(value.replace('Z', '+00:00')) class TaskBase(BaseModel): """任务基础模型""" title: str = Field(..., max_length=200) description: Optional[str] = None priority: str = Field(default="q4", pattern="^(q1|q2|q3|q4)$") due_date: Optional[datetime] = None category_id: Optional[int] = None @field_validator('due_date', mode='before') @classmethod def parse_due_date(cls, v): return parse_datetime(v) class TaskCreate(TaskBase): """创建任务请求模型""" tag_ids: Optional[List[int]] = [] class TaskUpdate(BaseModel): """更新任务请求模型 通过 exclude_unset=True 区分"前端没传"和"前端传了 null": - 前端没传某个字段 -> model_dump 结果中不包含该 key -> 不修改 - 前端传了 null -> model_dump 结果中包含 key: None -> 视为"清空" """ title: Optional[str] = Field(None, max_length=200) description: Optional[str] = None priority: Optional[str] = Field(None, pattern="^(q1|q2|q3|q4)$") due_date: Optional[datetime] = None is_completed: Optional[bool] = None category_id: Optional[int] = None tag_ids: Optional[List[int]] = None @field_validator('due_date', mode='before') @classmethod def parse_due_date(cls, v): return parse_datetime(v) @property def clearable_fields(self) -> set: """允许被显式清空(设为 None)的字段集合""" return {'description', 'due_date', 'category_id'} class TaskResponse(TaskBase): """任务响应模型""" id: int is_completed: bool created_at: datetime updated_at: datetime category: Optional[CategoryResponse] = None tags: List[TagResponse] = [] class Config: from_attributes = True