初次提交
This commit is contained in:
2
backend/app/schemas/__init__.py
Normal file
2
backend/app/schemas/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from .auth import *
|
||||
from .file import *
|
||||
165
backend/app/schemas/auth.py
Normal file
165
backend/app/schemas/auth.py
Normal file
@@ -0,0 +1,165 @@
|
||||
from pydantic import BaseModel, EmailStr, Field, validator
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
import re
|
||||
|
||||
class UserRegister(BaseModel):
|
||||
"""用户注册请求模型"""
|
||||
username: str = Field(..., min_length=3, max_length=50, description="用户名")
|
||||
email: EmailStr = Field(..., description="邮箱地址")
|
||||
password: str = Field(..., min_length=6, max_length=128, description="密码")
|
||||
confirm_password: str = Field(..., min_length=6, max_length=128, description="确认密码")
|
||||
|
||||
@validator('username')
|
||||
def validate_username(cls, v):
|
||||
"""验证用户名格式"""
|
||||
if not re.match(r'^[a-zA-Z0-9_]+$', v):
|
||||
raise ValueError('用户名只能包含字母、数字和下划线')
|
||||
if v.startswith('_') or v.endswith('_'):
|
||||
raise ValueError('用户名不能以下划线开头或结尾')
|
||||
return v
|
||||
|
||||
@validator('confirm_password')
|
||||
def passwords_match(cls, v, values):
|
||||
"""验证密码确认"""
|
||||
if 'password' in values and v != values['password']:
|
||||
raise ValueError('两次输入的密码不一致')
|
||||
return v
|
||||
|
||||
@validator('password')
|
||||
def validate_password_length(cls, v):
|
||||
"""验证密码长度"""
|
||||
if len(v) <= 5:
|
||||
raise ValueError('密码长度必须大于5个字符')
|
||||
return v
|
||||
|
||||
class UserLogin(BaseModel):
|
||||
"""用户登录请求模型"""
|
||||
username: str = Field(..., description="用户名或邮箱")
|
||||
password: str = Field(..., min_length=1, description="密码")
|
||||
|
||||
class TokenResponse(BaseModel):
|
||||
"""令牌响应模型"""
|
||||
access_token: str = Field(..., description="访问令牌")
|
||||
refresh_token: str = Field(..., description="刷新令牌")
|
||||
token_type: str = Field(default="bearer", description="令牌类型")
|
||||
expires_in: int = Field(..., description="访问令牌过期时间(秒)")
|
||||
|
||||
class UserResponse(BaseModel):
|
||||
"""用户信息响应模型"""
|
||||
id: int
|
||||
username: str
|
||||
email: str
|
||||
avatar_url: Optional[str] = None
|
||||
storage_quota: int
|
||||
storage_used: int
|
||||
is_active: bool
|
||||
is_verified: bool
|
||||
last_login_at: Optional[datetime] = None
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
class LoginResponse(BaseModel):
|
||||
"""登录响应模型"""
|
||||
user: UserResponse = Field(..., description="用户信息")
|
||||
tokens: TokenResponse = Field(..., description="令牌信息")
|
||||
|
||||
class TokenRefresh(BaseModel):
|
||||
"""令牌刷新请求模型"""
|
||||
refresh_token: str = Field(..., description="刷新令牌")
|
||||
|
||||
class PasswordChange(BaseModel):
|
||||
"""修改密码请求模型"""
|
||||
current_password: str = Field(..., description="当前密码")
|
||||
new_password: str = Field(..., min_length=8, max_length=128, description="新密码")
|
||||
confirm_password: str = Field(..., min_length=8, max_length=128, description="确认新密码")
|
||||
|
||||
@validator('confirm_password')
|
||||
def passwords_match(cls, v, values):
|
||||
"""验证密码确认"""
|
||||
if 'new_password' in values and v != values['new_password']:
|
||||
raise ValueError('密码确认不匹配')
|
||||
return v
|
||||
|
||||
@validator('new_password')
|
||||
def validate_password_strength(cls, v):
|
||||
"""验证密码强度"""
|
||||
errors = []
|
||||
|
||||
if len(v) < 8:
|
||||
errors.append("密码长度至少8位")
|
||||
|
||||
if not any(c.islower() for c in v):
|
||||
errors.append("密码必须包含至少一个小写字母")
|
||||
|
||||
if not any(c.isupper() for c in v):
|
||||
errors.append("密码必须包含至少一个大写字母")
|
||||
|
||||
if not any(c.isdigit() for c in v):
|
||||
errors.append("密码必须包含至少一个数字")
|
||||
|
||||
special_chars = "!@#$%^&*()_+-=[]{}|;:,.<>?"
|
||||
if not any(c in special_chars for c in v):
|
||||
errors.append("密码必须包含至少一个特殊字符")
|
||||
|
||||
if errors:
|
||||
raise ValueError('; '.join(errors))
|
||||
|
||||
return v
|
||||
|
||||
class PasswordReset(BaseModel):
|
||||
"""密码重置请求模型"""
|
||||
email: EmailStr = Field(..., description="邮箱地址")
|
||||
|
||||
class PasswordResetConfirm(BaseModel):
|
||||
"""密码重置确认模型"""
|
||||
token: str = Field(..., description="重置令牌")
|
||||
new_password: str = Field(..., min_length=8, max_length=128, description="新密码")
|
||||
confirm_password: str = Field(..., min_length=8, max_length=128, description="确认新密码")
|
||||
|
||||
@validator('confirm_password')
|
||||
def passwords_match(cls, v, values):
|
||||
"""验证密码确认"""
|
||||
if 'new_password' in values and v != values['new_password']:
|
||||
raise ValueError('密码确认不匹配')
|
||||
return v
|
||||
|
||||
@validator('new_password')
|
||||
def validate_password_strength(cls, v):
|
||||
"""验证密码强度"""
|
||||
errors = []
|
||||
|
||||
if len(v) < 8:
|
||||
errors.append("密码长度至少8位")
|
||||
|
||||
if not any(c.islower() for c in v):
|
||||
errors.append("密码必须包含至少一个小写字母")
|
||||
|
||||
if not any(c.isupper() for c in v):
|
||||
errors.append("密码必须包含至少一个大写字母")
|
||||
|
||||
if not any(c.isdigit() for c in v):
|
||||
errors.append("密码必须包含至少一个数字")
|
||||
|
||||
special_chars = "!@#$%^&*()_+-=[]{}|;:,.<>?"
|
||||
if not any(c in special_chars for c in v):
|
||||
errors.append("密码必须包含至少一个特殊字符")
|
||||
|
||||
if errors:
|
||||
raise ValueError('; '.join(errors))
|
||||
|
||||
return v
|
||||
|
||||
class ApiResponse(BaseModel):
|
||||
"""标准API响应模型"""
|
||||
success: bool = Field(..., description="操作是否成功")
|
||||
message: str = Field(..., description="响应消息")
|
||||
data: Optional[dict] = Field(None, description="响应数据")
|
||||
error: Optional[dict] = Field(None, description="错误信息")
|
||||
|
||||
class Config:
|
||||
json_encoders = {
|
||||
datetime: lambda v: v.isoformat() if v else None
|
||||
}
|
||||
148
backend/app/schemas/file.py
Normal file
148
backend/app/schemas/file.py
Normal file
@@ -0,0 +1,148 @@
|
||||
from pydantic import BaseModel, Field, validator
|
||||
from typing import Optional, List
|
||||
from datetime import datetime
|
||||
import re
|
||||
|
||||
class FileUploadRequest(BaseModel):
|
||||
"""文件上传请求"""
|
||||
description: Optional[str] = Field(None, max_length=500, description="文件描述")
|
||||
tags: Optional[str] = Field(None, max_length=200, description="文件标签,用逗号分隔")
|
||||
is_public: bool = Field(False, description="是否公开分享")
|
||||
|
||||
@validator('tags')
|
||||
def validate_tags(cls, v):
|
||||
if v:
|
||||
# 验证标签格式,只允许字母、数字、中文、下划线、中划线
|
||||
tags = v.split(',')
|
||||
for tag in tags:
|
||||
tag = tag.strip()
|
||||
if not re.match(r'^[\w\u4e00-\u9fa5-]+$', tag):
|
||||
raise ValueError(f"标签 '{tag}' 格式不正确,只允许字母、数字、中文、下划线、中划线")
|
||||
if len(tag) > 20:
|
||||
raise ValueError(f"标签 '{tag}' 长度不能超过20个字符")
|
||||
return v
|
||||
|
||||
class FileResponse(BaseModel):
|
||||
"""文件响应"""
|
||||
id: int
|
||||
user_id: int
|
||||
filename: str
|
||||
original_filename: str
|
||||
file_size: int
|
||||
mime_type: str
|
||||
file_hash: str
|
||||
is_public: bool
|
||||
download_count: int
|
||||
description: Optional[str] = None
|
||||
tags: Optional[str] = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
last_accessed_at: Optional[datetime] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
class FileListResponse(BaseModel):
|
||||
"""文件列表响应"""
|
||||
files: List[FileResponse]
|
||||
total: int
|
||||
page: int
|
||||
size: int
|
||||
pages: int
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
class FileListRequest(BaseModel):
|
||||
"""文件列表请求"""
|
||||
user_id: int = Field(..., description="用户ID")
|
||||
page: int = Field(1, ge=1, description="页码")
|
||||
size: int = Field(20, ge=1, le=100, description="每页数量")
|
||||
|
||||
class FileIdRequest(BaseModel):
|
||||
"""文件ID请求"""
|
||||
user_id: int = Field(..., description="用户ID")
|
||||
file_id: int = Field(..., description="文件ID")
|
||||
|
||||
class StorageInfoRequest(BaseModel):
|
||||
"""存储信息请求"""
|
||||
user_id: int = Field(..., description="用户ID")
|
||||
|
||||
class FileUpdateRequest(BaseModel):
|
||||
"""文件更新请求"""
|
||||
description: Optional[str] = Field(None, max_length=500, description="文件描述")
|
||||
tags: Optional[str] = Field(None, max_length=200, description="文件标签,用逗号分隔")
|
||||
is_public: Optional[bool] = Field(None, description="是否公开分享")
|
||||
|
||||
@validator('tags')
|
||||
def validate_tags(cls, v):
|
||||
if v:
|
||||
tags = v.split(',')
|
||||
for tag in tags:
|
||||
tag = tag.strip()
|
||||
if not re.match(r'^[\w\u4e00-\u9fa5-]+$', tag):
|
||||
raise ValueError(f"标签 '{tag}' 格式不正确,只允许字母、数字、中文、下划线、中划线")
|
||||
if len(tag) > 20:
|
||||
raise ValueError(f"标签 '{tag}' 长度不能超过20个字符")
|
||||
return v
|
||||
|
||||
class FileSearchRequest(BaseModel):
|
||||
"""文件搜索请求"""
|
||||
filename: Optional[str] = Field(None, description="文件名搜索")
|
||||
tags: Optional[str] = Field(None, description="标签搜索,用逗号分隔")
|
||||
mime_type: Optional[str] = Field(None, description="MIME类型过滤")
|
||||
is_public: Optional[bool] = Field(None, description="是否公开文件")
|
||||
start_date: Optional[datetime] = Field(None, description="开始日期")
|
||||
end_date: Optional[datetime] = Field(None, description="结束日期")
|
||||
min_size: Optional[int] = Field(None, ge=0, description="最小文件大小(字节)")
|
||||
max_size: Optional[int] = Field(None, ge=0, description="最大文件大小(字节)")
|
||||
|
||||
class FileInfo(BaseModel):
|
||||
"""文件信息"""
|
||||
id: int
|
||||
filename: str
|
||||
original_filename: str
|
||||
file_size: int
|
||||
mime_type: str
|
||||
file_hash: str
|
||||
is_image: bool
|
||||
is_document: bool
|
||||
file_extension: str
|
||||
size_formatted: str
|
||||
is_public: bool = False
|
||||
download_count: int = 0
|
||||
description: Optional[str] = None
|
||||
tags: Optional[str] = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
last_accessed_at: Optional[datetime] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
class UploadResponse(BaseModel):
|
||||
"""文件上传响应"""
|
||||
file_info: FileResponse
|
||||
message: str
|
||||
success: bool
|
||||
|
||||
class DeleteResponse(BaseModel):
|
||||
"""删除文件响应"""
|
||||
message: str
|
||||
success: bool
|
||||
|
||||
class StorageInfo(BaseModel):
|
||||
"""存储信息"""
|
||||
total_quota: int
|
||||
used_space: int
|
||||
available_space: int
|
||||
usage_percentage: float
|
||||
file_count: int
|
||||
|
||||
# 通用API响应格式
|
||||
class ApiResponse(BaseModel):
|
||||
"""API响应"""
|
||||
success: bool
|
||||
message: str
|
||||
data: Optional[dict] = None
|
||||
code: Optional[str] = None
|
||||
Reference in New Issue
Block a user