from pydantic import BaseModel, Field, field_validator from datetime import date, datetime, timezone from typing import Optional, List def parse_date(value): """解析日期字符串""" if value is None or value == '': return None if isinstance(value, date): return value if isinstance(value, datetime): return value.date() formats = [ '%Y-%m-%d', '%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).date() except ValueError: continue try: return date.fromisoformat(value) except ValueError: raise ValueError(f"无法解析日期: {value}") # ============ 纪念日分类 Schema ============ class AnniversaryCategoryBase(BaseModel): """纪念日分类基础模型""" name: str = Field(..., max_length=50) icon: str = Field(default="calendar", max_length=50) color: str = Field(default="#FFB7C5", max_length=20) sort_order: int = Field(default=0) class AnniversaryCategoryCreate(AnniversaryCategoryBase): """创建纪念日分类请求模型""" pass class AnniversaryCategoryUpdate(BaseModel): """更新纪念日分类请求模型""" name: Optional[str] = Field(None, max_length=50) icon: Optional[str] = Field(None, max_length=50) color: Optional[str] = Field(None, max_length=20) sort_order: Optional[int] = None class AnniversaryCategoryResponse(AnniversaryCategoryBase): """纪念日分类响应模型""" id: int class Config: from_attributes = True # ============ 纪念日 Schema ============ class AnniversaryBase(BaseModel): """纪念日基础模型""" title: str = Field(..., max_length=200) date: date year: Optional[int] = None category_id: Optional[int] = None description: Optional[str] = None is_recurring: bool = Field(default=True) remind_days_before: int = Field(default=3) @field_validator('date', mode='before') @classmethod def parse_anniversary_date(cls, v): result = parse_date(v) if result is None: raise ValueError("纪念日日期不能为空") return result class AnniversaryCreate(AnniversaryBase): """创建纪念日请求模型""" pass class AnniversaryUpdate(BaseModel): """更新纪念日请求模型""" title: Optional[str] = Field(None, max_length=200) date: Optional[date] = None year: Optional[int] = None category_id: Optional[int] = None description: Optional[str] = None is_recurring: Optional[bool] = None remind_days_before: Optional[int] = None @field_validator('date', mode='before') @classmethod def parse_anniversary_date(cls, v): if v is None: return None return parse_date(v) @property def clearable_fields(self) -> set: """允许被显式清空(设为 None)的字段集合""" return {'description', 'category_id', 'year'} class AnniversaryResponse(AnniversaryBase): """纪念日响应模型""" id: int created_at: datetime updated_at: datetime category: Optional[AnniversaryCategoryResponse] = None next_date: Optional[date] = None days_until: Optional[int] = None year_count: Optional[int] = None class Config: from_attributes = True