from pydantic import BaseModel, Field, field_validator class TodoCreate(BaseModel): title: str = Field(..., min_length=1, max_length=100) class TodoUpdate(BaseModel): title: str | None = Field(default=None, min_length=1, max_length=100) done: bool | None = None class Todo(BaseModel): id: int title: str done: bool = False class UserCreate(BaseModel): name: str = Field(..., min_length=1, max_length=80) phone: str = Field(..., min_length=5, max_length=20, pattern=r"^[0-9+\-\s]+$") nickname: str | None = Field(default=None, max_length=60) avatar: str | None = Field(default=None, max_length=300) is_admin: bool = False total_spent: float = Field(default=0, ge=0) class UserUpdate(BaseModel): name: str | None = Field(default=None, min_length=1, max_length=80) phone: str | None = Field(default=None, min_length=5, max_length=20, pattern=r"^[0-9+\-\s]+$") nickname: str | None = Field(default=None, max_length=60) avatar: str | None = Field(default=None, max_length=300) is_admin: bool | None = None total_spent: float | None = Field(default=None, ge=0) class User(BaseModel): id: int registered_at: str name: str phone: str | None = None nickname: str | None = None avatar: str | None = None is_admin: bool = False total_spent: float = 0 class AuthUser(BaseModel): id: int username: str name: str is_admin: bool = False class CaptchaResponse(BaseModel): captcha_id: str challenge: str expires_in: int class LoginRequest(BaseModel): username: str = Field(..., min_length=1, max_length=80) password: str = Field(..., min_length=1, max_length=128) captcha_id: str | None = Field(default=None, max_length=80) captcha_code: str | None = Field(default=None, max_length=20) class LoginResponse(BaseModel): expires_in: int user: AuthUser requires_second_verification: bool = False security_notice: str | None = None class TempleCreate(BaseModel): name: str = Field(..., min_length=1, max_length=100) location: str | None = Field(default=None, max_length=160) contact_phone: str | None = Field(default=None, max_length=30) description: str | None = Field(default=None, max_length=500) is_active: bool = True class TempleUpdate(BaseModel): name: str | None = Field(default=None, min_length=1, max_length=100) location: str | None = Field(default=None, max_length=160) contact_phone: str | None = Field(default=None, max_length=30) description: str | None = Field(default=None, max_length=500) is_active: bool | None = None class Temple(BaseModel): id: int name: str location: str | None = None contact_phone: str | None = None description: str | None = None is_active: bool = True class ProductSpec(BaseModel): name: str = Field(..., min_length=1, max_length=80) price: float = Field(default=0, ge=0) stock: int = Field(default=0, ge=0) sort_order: int = Field(default=0, ge=0) is_chaodu: bool = False class ProductBase(BaseModel): name: str = Field(..., min_length=1, max_length=120) alias: str | None = Field(default=None, max_length=80) description: str | None = Field(default=None, max_length=500) payment_button_name: str = Field(default="立即支付", min_length=1, max_length=30) donation_button_name: str = Field(default="随喜", min_length=1, max_length=30) sort_order: int = Field(default=0, ge=0) show_participant_count: bool = False cover_image: str | None = Field(default=None, max_length=500) images: list[str] = Field(default_factory=list, max_length=10) listed_at: str | None = Field(default=None, max_length=30) sale_starts_at: str | None = Field(default=None, max_length=30) sale_ends_at: str | None = Field(default=None, max_length=30) delisted_at: str | None = Field(default=None, max_length=30) show_countdown: bool = False show_on_home: bool = False specs: list[ProductSpec] = Field(default_factory=list) details_html: str | None = None share_title: str | None = Field(default=None, max_length=120) share_description: str | None = Field(default=None, max_length=300) share_image: str | None = Field(default=None, max_length=500) merit_certificate_reserved: bool = False auto_process: bool = False is_active: bool = True sales_count: int = Field(default=0, ge=0) @field_validator("images") @classmethod def validate_images_count(cls, value: list[str]) -> list[str]: if len(value) > 10: raise ValueError("Product images cannot exceed 10") return value class ProductCreate(ProductBase): pass class ProductUpdate(BaseModel): name: str | None = Field(default=None, min_length=1, max_length=120) alias: str | None = Field(default=None, max_length=80) description: str | None = Field(default=None, max_length=500) payment_button_name: str | None = Field(default=None, min_length=1, max_length=30) donation_button_name: str | None = Field(default=None, min_length=1, max_length=30) sort_order: int | None = Field(default=None, ge=0) show_participant_count: bool | None = None cover_image: str | None = Field(default=None, max_length=500) images: list[str] | None = Field(default=None, max_length=10) listed_at: str | None = Field(default=None, max_length=30) sale_starts_at: str | None = Field(default=None, max_length=30) sale_ends_at: str | None = Field(default=None, max_length=30) delisted_at: str | None = Field(default=None, max_length=30) show_countdown: bool | None = None show_on_home: bool | None = None specs: list[ProductSpec] | None = None details_html: str | None = None share_title: str | None = Field(default=None, max_length=120) share_description: str | None = Field(default=None, max_length=300) share_image: str | None = Field(default=None, max_length=500) merit_certificate_reserved: bool | None = None auto_process: bool | None = None is_active: bool | None = None sales_count: int | None = Field(default=None, ge=0) @field_validator("images") @classmethod def validate_images_count(cls, value: list[str] | None) -> list[str] | None: if value is not None and len(value) > 10: raise ValueError("Product images cannot exceed 10") return value class Product(BaseModel): id: int temple_id: int name: str alias: str | None = None description: str | None = None payment_button_name: str = "立即支付" donation_button_name: str = "随喜" sort_order: int = 0 show_participant_count: bool = False cover_image: str | None = None images: list[str] = Field(default_factory=list) listed_at: str | None = None sale_starts_at: str | None = None sale_ends_at: str | None = None delisted_at: str | None = None show_countdown: bool = False show_on_home: bool = False specs: list[ProductSpec] = Field(default_factory=list) details_html: str | None = None share_title: str | None = None share_description: str | None = None share_image: str | None = None merit_certificate_reserved: bool = False auto_process: bool = False is_active: bool = True sales_count: int = 0 status: str class UploadResponse(BaseModel): url: str object_key: str class RitualServiceCreate(BaseModel): name: str = Field(..., min_length=1, max_length=120) description: str | None = Field(default=None, max_length=500) price: float = Field(default=0, ge=0) is_active: bool = True class RitualServiceUpdate(BaseModel): name: str | None = Field(default=None, min_length=1, max_length=120) description: str | None = Field(default=None, max_length=500) price: float | None = Field(default=None, ge=0) is_active: bool | None = None class RitualService(BaseModel): id: int temple_id: int name: str description: str | None = None price: float = 0 is_active: bool = True class OrderCreate(BaseModel): customer_name: str = Field(..., min_length=1, max_length=80) customer_phone: str | None = Field(default=None, max_length=30) order_type: str = Field(default="product", pattern=r"^(product|ritual)$") item_name: str = Field(..., min_length=1, max_length=120) amount: float = Field(default=0, ge=0) status: str = Field(default="pending", pattern=r"^(pending|paid|completed|cancelled)$") class OrderUpdate(BaseModel): customer_name: str | None = Field(default=None, min_length=1, max_length=80) customer_phone: str | None = Field(default=None, max_length=30) order_type: str | None = Field(default=None, pattern=r"^(product|ritual)$") item_name: str | None = Field(default=None, min_length=1, max_length=120) amount: float | None = Field(default=None, ge=0) status: str | None = Field(default=None, pattern=r"^(pending|paid|completed|cancelled)$") class Order(BaseModel): id: int temple_id: int customer_name: str customer_phone: str | None = None order_type: str item_name: str amount: float = 0 status: str created_at: str class WeChatJSAPIPayRequest(BaseModel): order_id: int openid: str = Field(..., min_length=1, max_length=128) description: str | None = Field(default=None, max_length=127) class WeChatJSAPIPayResponse(BaseModel): appId: str timeStamp: str nonceStr: str package: str signType: str paySign: str out_trade_no: str prepay_id: str