# models.py import re from pydantic import BaseModel, Field, field_validator # 🧹 P2修复:pydantic v2 已废弃 v1 风格 validator from typing import Optional, List, Literal from profile_media import normalize_profile_media def _validate_email_format(email: str) -> str: """邮箱格式校验""" if not re.match(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', email): raise ValueError('邮箱格式不正确') return email def _validate_password_length(password: str) -> str: """密码长度校验:6-64字符(兼容旧用户短密码)""" if len(password) < 6: raise ValueError('密码长度不能少于6个字符') if len(password) > 64: raise ValueError('密码长度不能超过64个字符') return password class SendCodeRequest(BaseModel): contact: str = Field(..., min_length=1, max_length=254) contact_type: Literal["email", "phone"] action_type: Literal["register", "reset", "recover", "withdraw"] account: Optional[str] = None class UserRegister(BaseModel): account: str password: str email: str phone: Optional[str] = "" name: str gender: str code: str avatarDataUrl: Optional[str] = None age: Optional[int] = None country: Optional[str] = None region: Optional[str] = None intro: Optional[str] = None # 🧹 P2修复:v2 中下划线前缀属性会被当作私有属性而非校验器,改用 field_validator 显式挂载(校验逻辑不变) @field_validator('password') @classmethod def _check_password(cls, v): return _validate_password_length(v) @field_validator('email') @classmethod def _check_email(cls, v): return _validate_email_format(v) @field_validator('avatarDataUrl') @classmethod def _check_avatar_url(cls, value): return normalize_profile_media(value) class UserLogin(BaseModel): account: str password: str remember: Optional[bool] = True # 保持登录选项:True=30天, False=24小时 @field_validator('password') @classmethod def _check_password(cls, v): return _validate_password_length(v) class UserUpdate(BaseModel): name: Optional[str] = None intro: Optional[str] = None age: Optional[int] = None birthday: Optional[str] = None # 🚀 出生日期 country: Optional[str] = None region: Optional[str] = None gender: Optional[str] = None avatarDataUrl: Optional[str] = None bannerUrl: Optional[str] = None # 🚀 个人资料卡背景图云端URL @field_validator('avatarDataUrl', 'bannerUrl') @classmethod def _check_media_url(cls, value): return normalize_profile_media(value) class SettingsPrivacy(BaseModel): model_config = {"extra": "forbid"} follows: bool = Field(strict=True) likes: bool = Field(strict=True) favorites: bool = Field(strict=True) downloads: bool = Field(strict=True) class UserSettings(UserUpdate): model_config = {"extra": "forbid"} privacy: SettingsPrivacy # 🧹 P2清理:移除零引用的 PasswordReset 模型(重置密码实际走 UserUpdate/专用端点) class CommentCreate(BaseModel): item_id: str author: str content: str reply_to_user: Optional[str] = None parent_id: Optional[str] = None class ItemCreate(BaseModel): request_id: Optional[str] = Field(default=None, strict=True, pattern=r'^[0-9a-f]{32}$') type: str title: str shortDesc: str fullDesc: str link: str coverUrl: Optional[str] = None imageUrls: Optional[List[str]] = [] author: str price: int = Field(default=0, ge=0, strict=True) github_token: Optional[str] = None netdisk_password: Optional[str] = None # ☁️ 网盘提取码(加密存储,购买后解密) is_netdisk: Optional[bool] = False # ☁️ 是否为网盘资源 is_original: Optional[bool] = False # 🎨 是否为原创作品 allow_refund: Optional[bool] = True # 💸 是否支持退款(默认支持) class ItemUpdate(BaseModel): expected_revision: Optional[int] = Field(default=None, ge=0, le=9007199254740991, strict=True) title: Optional[str] = None shortDesc: Optional[str] = None fullDesc: Optional[str] = None link: Optional[str] = None coverUrl: Optional[str] = None imageUrls: Optional[List[str]] = None price: Optional[int] = Field(default=None, ge=0, strict=True) github_token: Optional[str] = None netdisk_password: Optional[str] = None # ☁️ 网盘提取码 is_netdisk: Optional[bool] = None # ☁️ 是否为网盘资源 is_original: Optional[bool] = None # 🎨 是否为原创作品 allow_refund: Optional[bool] = None # 💸 是否支持退款 class FollowToggle(BaseModel): user_id: str target_account: str is_active: bool class PrivateMessage(BaseModel): sender: str receiver: str content: str class IdempotentPrivateMessage(PrivateMessage): client_request_id: str = Field(..., strict=True, min_length=32, max_length=32, pattern=r"^[0-9a-f]{32}$") class PrivacySettings(BaseModel): follows: bool likes: bool favorites: bool downloads: bool class InteractionToggle(BaseModel): item_id: str user_id: str action_type: str is_active: bool class RatingRequest(BaseModel): score: int # 1-5 @field_validator("score") @classmethod def validate_score(cls, v): if not isinstance(v, int): raise ValueError("score must be an integer between 1 and 5") if v < 1 or v > 5: raise ValueError("score must be between 1 and 5") return v class RechargeRequest(BaseModel): account: str amount: int = Field(..., ge=1, le=10000, strict=True) # 单次充值1-10000 method: Optional[str] = "alipay" class PurchaseRequest(BaseModel): account: str item_id: str expected_price: Optional[int] = Field(default=None, ge=0, strict=True) class PromptPurchaseRequest(BaseModel): expected_price: Optional[int] = Field(default=None, ge=0, strict=True) class TipRequest(BaseModel): sender_account: str target_account: str amount: int is_anonymous: bool item_id: Optional[str] = None # 🚀 新增:记录是为哪个作品打赏的 class WithdrawRequest(BaseModel): account: str amount: int = Field(..., ge=1, le=5000, strict=True) # 单次提现1-5000 alipayAccount: str = Field(..., min_length=1, max_length=256) real_name: str = Field(..., min_length=1, max_length=128) code: str = Field(..., pattern=r"^[0-9]{6}$") # ========================================== # 📝 任务榜数据模型 # ========================================== class TaskCreate(BaseModel): """ 发布新任务 """ title: str # 任务标题 description: str # 任务详情 referenceImages: Optional[List[str]] = [] # 参考图(最多6张) referenceLink: Optional[str] = None # 参考链接 totalPrice: int = Field(..., ge=0, le=2147483647, strict=True) # SQL整数范围 depositRatio: int = Field(..., strict=True) # 订金比例(10/20/30/50) deadline: str # 截止时间 (ISO格式) publisher: str # 发布者账号 class TaskUpdate(BaseModel): """ 更新任务 """ title: Optional[str] = None description: Optional[str] = None referenceImages: Optional[List[str]] = None referenceLink: Optional[str] = None totalPrice: Optional[int] = Field(None, ge=0, le=2147483647, strict=True) depositRatio: Optional[int] = Field(None, strict=True) deadline: Optional[str] = None # 🔧 P2修复:以下三个 body 模型修复前后端参数通道不匹配 # (前端以 JSON body 提交,旧版端点用裸标量参数会被 FastAPI 解析为 query 导致 422) class TaskSubmit(BaseModel): """ 接单者提交成果 """ deliverables: List[str] # 交付成果列表 note: Optional[str] = None # 提交说明 class TaskDispute(BaseModel): """ 发起申诉 """ reason: str # 申诉理由 evidence: Optional[List[str]] = None # 证据图片URL列表(最多6张) class TaskDisputeResponse(BaseModel): """ 被申诉方回应申诉 """ response: str # 回应内容 evidence: Optional[List[str]] = None # 证据图片URL列表(最多6张) # 🧹 P2清理:移除零引用的 TaskApply/TaskAssign/TaskAccept 模型(对应端点使用 query 参数且前端匹配) # ========================================== # 💬 讨论区数据模型(小红书风格图文社区) # ========================================== class PostCreate(BaseModel): """ 创建帖子 """ title: str # 标题 content: str # 正文/文案 cover_image: str # 封面图(第一张) images: Optional[List[str]] = [] # 图片列表(最多9张) author: str # 作者账号 is_original: Optional[bool] = False # 🎨 是否为原创作品 post_type: str = "image" # "image" 或 "video" video_url: Optional[str] = None # 视频 URL(仅 video 类型) class PostUpdate(BaseModel): """ 更新帖子 """ title: Optional[str] = None content: Optional[str] = None cover_image: Optional[str] = None images: Optional[List[str]] = None is_original: Optional[bool] = None # 🎨 是否为原创作品 post_type: Optional[str] = None # "image" 或 "video" video_url: Optional[str] = None # 视频 URL(仅 video 类型) # ========================================== # ✨ 提示词市场数据模型(图像/视频提示词) # ========================================== class PromptCreate(BaseModel): """ 发布提示词 """ prompt_type: str # "image"、"video" 或 "music"(所属子界面) category: str # 所属分类(人物/动作等) title: str # 标题 content: str # 公开介绍文案 prompt_text: str # 提示词正文(付费内容,服务端脱敏下发) cover_image: str # 封面图 media_type: str = "image" # "image" 或 "video"(示例媒体类型) video_url: Optional[str] = None # 示例视频 URL(仅 media_type=video) audio_url: Optional[str] = None # 示例音频 URL(仅音乐提示词,图片+音频方案;视频可选) images: Optional[List[str]] = None # 图像提示词图片列表(最多9张,首张为封面) is_original: bool = False # 原创作品标记 price: int = Field(default=0, ge=0, le=10000, strict=True) # 提示词定价(0=免费) tags: Optional[List[str]] = None # 自定义标签(最多10个,每个≤12字) class PromptUpdate(BaseModel): """ 更新提示词 """ category: Optional[str] = None title: Optional[str] = None content: Optional[str] = None prompt_text: Optional[str] = None cover_image: Optional[str] = None media_type: Optional[str] = None video_url: Optional[str] = None audio_url: Optional[str] = None # 示例音频 URL(仅音乐提示词;传 null 表示清除) images: Optional[List[str]] = None # 图像提示词图片列表(最多9张,首张为封面) is_original: Optional[bool] = None # 原创作品标记 price: Optional[int] = Field(default=None, ge=0, le=10000, strict=True) tags: Optional[List[str]] = None class PromptCategoryAdd(BaseModel): """ 添加提示词分类 """ prompt_type: str # "image"、"video" 或 "music" name: str # 分类名称(≤8字,去重)