初次提交

This commit is contained in:
2025-10-14 20:05:29 +08:00
commit 6e4e48fdd2
673 changed files with 437006 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
from .auth import *
from .file import *

165
backend/app/schemas/auth.py Normal file
View 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
View 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