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字,去重)