Spaces:
Running
Running
File size: 12,456 Bytes
13f2c73 8541ba4 b267d43 6d79d77 be93389 d5dffcf 8541ba4 4bd2c56 8541ba4 d3ae963 6d79d77 13f2c73 d3ae963 d5dffcf 13f2c73 d5dffcf 13f2c73 d5dffcf 8541ba4 b267d43 d5dffcf be93389 d5dffcf e3fab5f 8541ba4 b267d43 d5dffcf e904a47 d5dffcf e904a47 d5dffcf be93389 b9d11ff b267d43 d5dffcf c8e5eaa d5dffcf 0be9501 d5dffcf c8e5eaa 4c9deec c60e0ef 8f0ec20 8e0cd3e d5dffcf 13f2c73 c8e5eaa 13f2c73 c8e5eaa 13f2c73 c60e0ef 8f0ec20 8e0cd3e 13f2c73 d5dffcf 90d7999 52590d6 90d7999 71ce6d2 13f2c73 85494ee 856d1b0 b267d43 856d1b0 13f2c73 85494ee c8e5eaa 13f2c73 85494ee 012c1d3 c8e5eaa 012c1d3 13f2c73 f68778c 13f2c73 c8e5eaa b71f16d c8e5eaa b71f16d c8e5eaa b71f16d b267d43 b71f16d b267d43 b71f16d b267d43 b71f16d b267d43 b71f16d b267d43 c60e0ef 8f0ec20 c70d6e7 c60e0ef 8f0ec20 c70d6e7 7c4a0ac 52cacb2 7c4a0ac 7b79f99 ee72228 c8e5eaa 35b2edc 7c4a0ac 7b79f99 ee72228 c8e5eaa 35b2edc 7c4a0ac 52cacb2 c8e5eaa | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 | # 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字,去重)
|