first commit

This commit is contained in:
2026-07-18 20:32:51 +08:00
commit 77bf076ad0
34 changed files with 4068 additions and 0 deletions
+254
View File
@@ -0,0 +1,254 @@
import uuid
from datetime import datetime
from typing import Generic, Literal, TypeVar
from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator, model_validator
Role = Literal["user", "admin"]
CloudStatus = Literal["pending", "approved", "rejected"]
Rarity = Literal["common", "uncommon", "rare"]
class MessageOut(BaseModel):
message: str
class RegisterIn(BaseModel):
email: EmailStr
password: str = Field(min_length=8, max_length=128)
username: str = Field(min_length=2, max_length=32)
@field_validator("username")
@classmethod
def strip_username(cls, value: str) -> str:
value = value.strip()
if len(value) < 2:
raise ValueError("昵称至少需要 2 个字符")
return value
class LoginIn(BaseModel):
email: EmailStr
password: str = Field(min_length=1, max_length=128)
class TokenIn(BaseModel):
token: str = Field(min_length=20, max_length=512)
class ForgotPasswordIn(BaseModel):
email: EmailStr
class ResetPasswordIn(TokenIn):
password: str = Field(min_length=8, max_length=128)
class ChangePasswordIn(BaseModel):
password: str = Field(min_length=8, max_length=128)
class ProfileOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: uuid.UUID
username: str
avatar_url: str | None = None
role: Role
is_disabled: bool
created_at: datetime
class PublicProfileOut(BaseModel):
id: uuid.UUID
username: str
avatar_url: str | None = None
created_at: datetime
class UserOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: uuid.UUID
email: EmailStr
email_verified: bool
created_at: datetime
class AuthOut(BaseModel):
access_token: str
token_type: Literal["bearer"] = "bearer"
expires_in: int
user: UserOut
profile: ProfileOut
class MeOut(BaseModel):
user: UserOut
profile: ProfileOut
class ProfileUpdateIn(BaseModel):
username: str = Field(min_length=2, max_length=32)
@field_validator("username")
@classmethod
def strip_username(cls, value: str) -> str:
return value.strip()
class CloudTypeSummary(BaseModel):
id: int
name: str
name_en: str
rarity: Rarity
class CloudTypeOut(CloudTypeSummary):
genus: str
description: str | None
icon_url: str | None
created_at: datetime
class CloudOwnerOut(BaseModel):
id: uuid.UUID
username: str
class CloudOut(BaseModel):
id: uuid.UUID
user_id: uuid.UUID
cloud_type_id: int | None
custom_cloud_type: str | None
image_url: str
thumbnail_url: str
latitude: float | None
longitude: float | None
location_name: str | None
description: str | None
captured_at: datetime | None
status: CloudStatus
is_hidden: bool
created_at: datetime
updated_at: datetime
cloud_type: CloudTypeSummary | None
owner: CloudOwnerOut | None
cloud_type_name: str
cloud_type_rarity: Rarity
username: str
class CloudUpdateIn(BaseModel):
cloud_type_id: int | None = None
custom_cloud_type: str | None = Field(default=None, max_length=80)
latitude: float | None = Field(default=None, ge=-90, le=90)
longitude: float | None = Field(default=None, ge=-180, le=180)
location_name: str | None = Field(default=None, max_length=120)
description: str | None = Field(default=None, max_length=2000)
captured_at: datetime | None = None
is_hidden: bool | None = None
@model_validator(mode="after")
def validate_coordinates(self) -> "CloudUpdateIn":
provided = self.model_fields_set
if ("latitude" in provided) != ("longitude" in provided):
raise ValueError("经纬度必须同时提供")
if "latitude" in provided and ((self.latitude is None) != (self.longitude is None)):
raise ValueError("经纬度必须同时为空或同时有值")
return self
class BadgeOut(BaseModel):
cloud_type_id: int
cloud_name: str
cloud_name_en: str
rarity: Rarity
unlocked_at: datetime
class CloudCreateOut(BaseModel):
cloud: CloudOut
unlocked_badge: BadgeOut | None
T = TypeVar("T")
class PageOut(BaseModel, Generic[T]):
items: list[T]
page: int
page_size: int
total: int
total_pages: int
class BatchIdsIn(BaseModel):
ids: list[uuid.UUID] = Field(min_length=1, max_length=100)
@field_validator("ids")
@classmethod
def unique_ids(cls, value: list[uuid.UUID]) -> list[uuid.UUID]:
return list(dict.fromkeys(value))
class DeleteResultOut(BaseModel):
deleted: int
class CollectionCloudOut(BaseModel):
id: uuid.UUID
image_url: str
thumbnail_url: str
captured_at: datetime | None
created_at: datetime
location_name: str | None
class CollectionOut(BaseModel):
id: uuid.UUID
user_id: uuid.UUID
cloud_type_id: int
first_cloud_id: uuid.UUID | None
unlocked_at: datetime
cloud_type: CloudTypeSummary
first_cloud: CollectionCloudOut | None
class AdminStatsOut(BaseModel):
users: int
images: int
today_uploads: int
pending: int
approved: int
rejected: int
hidden: int
class AdminUserOut(BaseModel):
user: UserOut
profile: ProfileOut
class AdminUserUpdateIn(BaseModel):
role: Role | None = None
is_disabled: bool | None = None
@model_validator(mode="after")
def require_change(self) -> "AdminUserUpdateIn":
if not self.model_fields_set:
raise ValueError("至少需要提供一个修改字段")
return self
class AdminCloudStatusIn(BatchIdsIn):
status: CloudStatus
class AdminCloudVisibilityIn(BatchIdsIn):
is_hidden: bool
class BatchUpdateOut(BaseModel):
updated: int