from pydantic import BaseModel, EmailStr, Field class Destination(BaseModel): id: str slug: str name: str country: str emoji: str tag: str description: str region: str cost: int speed: int temperature: int rating: float hue: int = 170 nomads_count: str = "5,000+" highlights: list[str] = [] map_x: float = 0 map_y: float = 0 class Visa(BaseModel): id: str country: str flag: str name: str badge: str badge_type: str duration: str income_req: str approval_time: str extra: str difficulty: int difficulty_label: str class FAQ(BaseModel): id: str question: str answer: str order: int = 0 class Testimonial(BaseModel): id: str avatar: str content: str author: str role: str rating: int = 5 class Tool(BaseModel): id: str emoji: str name: str description: str tags: list[str] category: str class SubscribeRequest(BaseModel): email: EmailStr class SubscribeResponse(BaseModel): success: bool message: str class CostCalculatorRequest(BaseModel): destination_slug: str housing: str = "mid" # budget | mid | premium months: int = Field(default=1, ge=1, le=24) class CostCalculatorResponse(BaseModel): destination: str emoji: str months: int breakdown: dict[str, int] total: int per_month: int class CompareRequest(BaseModel): slugs: list[str] = Field(..., min_length=2, max_length=4) class StatsResponse(BaseModel): countries: int avg_cost: int satisfaction: int total_nomads: str active_today: int class ChartData(BaseModel): labels: list[str] datasets: list[dict] class BlogPost(BaseModel): id: str slug: str title: str excerpt: str emoji: str author: str published_at: str read_time: int tags: list[str] = [] class BlogPostDetail(BlogPost): content: str class AuthRegister(BaseModel): email: EmailStr password: str = Field(min_length=6) name: str = Field(min_length=1) class AuthLogin(BaseModel): email: EmailStr password: str class UserProfile(BaseModel): id: str email: str name: str avatar: str = "🧑‍💻" class AuthResponse(BaseModel): token: str user: UserProfile class FavoriteRequest(BaseModel): destination_slug: str class FavoriteResponse(BaseModel): slugs: list[str] class ProfileStats(BaseModel): favorites_count: int destinations_explored: int member_since: str nomad_level: str class TripItemModel(BaseModel): slug: str name: str country: str emoji: str cost: float months: int = 1 note: str = "" class PlanMetaModel(BaseModel): title: str = "我的旅居计划" startMonth: str = "" monthlyBudget: float = 0 checklist: dict[str, bool] = Field(default_factory=dict) class UserPlanPayload(BaseModel): items: list[TripItemModel] = Field(default_factory=list) meta: PlanMetaModel = Field(default_factory=PlanMetaModel) updated_at: int = 0 class SearchResult(BaseModel): type: str # destination | blog | visa | faq title: str subtitle: str emoji: str url: str class Meetup(BaseModel): id: str title: str city: str destination_slug: str = "" emoji: str date: str time: str venue: str description: str mode: str # online | offline | hybrid access_level: str = "public" mirotalkRoom: str = "" loungeChannel: str = "" rsvp_count: int = 0 max_attendees: int = 50 organizer: str = "" tags: list[str] = [] is_upcoming: bool = True class Discussion(BaseModel): id: str title: str excerpt: str author: str author_emoji: str = "🧑‍💻" category: str reply_count: int = 0 like_count: int = 0 is_pinned: bool = False created_at: str tags: list[str] = [] class DiscussionReply(BaseModel): id: str author: str author_emoji: str = "🧑‍💻" content: str created_at: str like_count: int = 0 class DiscussionDetail(Discussion): replies: list[DiscussionReply] = [] class NomadRoute(BaseModel): id: str slug: str title: str duration_days: int budget: int description: str stops: list[str] emoji: str = "🗺️" class NextStopRequest(BaseModel): budget: int = Field(default=8000, ge=2000, le=50000) internet: int = Field(default=50, ge=10, le=500) climate: str = "mild" # warm | mild | cool tags: list[str] = Field(default_factory=list) priority: str = "balanced" # cost | speed | community | balanced limit: int = Field(default=12, ge=1, le=24) class RecommendedDestination(Destination): match_score: float = 0 match_reasons: list[str] = [] class NextStopResponse(BaseModel): items: list[RecommendedDestination] routes: list[NomadRoute] = [] class MeetupRsvpRequest(BaseModel): meetup_id: str name: str = Field(default="访客", min_length=1) # ── Social / matching / chat ── class MatchProfile(BaseModel): id: str userId: str = "" name: str location: str = "" citySlug: str = "" gender: str = "" single: str = "" bio: str = "" photo: str = "🧑‍💻" tags: list[str] = [] lookingFor: list[str] = [] intent: str = "" class SwipeRequest(BaseModel): profileId: str action: str = "like" # like | dislike | superlike intent: str = "friends" class SwipeResponse(BaseModel): ok: bool matched: bool = False match: dict | None = None error: str | None = None class MatchQuota(BaseModel): vip: bool limit: int used: int remaining: int class JoinMemberRequest(BaseModel): city: str = "" citySlug: str = "" gender: str = "" single: str = "" bio: str = "" lookingFor: list[str] = Field(default_factory=lambda: ["friends", "explore"]) photo: str = "🧑‍💻" class ChatPeer(BaseModel): id: str = "" userId: str = "" name: str = "" photo: str = "🧑‍💻" location: str = "" class ConversationItem(BaseModel): id: str type: str = "direct" intent: str = "" lastMessageAt: str = "" lastMessagePreview: str = "" unreadCount: int = 0 peer: MatchProfile | None = None class ChatMessage(BaseModel): id: str conversationId: str = "" senderId: str body: str createdAt: str = "" mine: bool = False class SendMessageRequest(BaseModel): body: str = Field(min_length=1, max_length=2000) class VipStatus(BaseModel): vip: bool expires_at: int = 0 class PayCreateRequest(BaseModel): pay_type: str = "join" # join | vip | meetup return_url: str = "" provider: str = "zpay" class PayCreateResponse(BaseModel): order_id: str amount: int redirect_url: str status: str class PayStatusResponse(BaseModel): order_id: str status: str paid: bool class MeetupSession(BaseModel): canJoin: bool lockReason: str = "" displayName: str = "" videoUrl: str = "" chatUrl: str = "" loungeChannel: str = "" mirotalkRoom: str = "" meetingProvider: str = "mirotalk" mode: str = "" class DigitalLesson(BaseModel): title: str moduleIndex: int lessonIndex: int duration: str = "" free: bool = False content: str = "" class DigitalJob(BaseModel): id: str title: str company: str location: str type: str salary: str tags: list[str] = [] url: str = "" class CreateDiscussionRequest(BaseModel): title: str = Field(min_length=4, max_length=120) excerpt: str = "" content: str = "" category: str = "社区" tags: list[str] = Field(default_factory=list) author_emoji: str = "🧑‍💻" class CreateReplyRequest(BaseModel): content: str = Field(min_length=1, max_length=4000) author_emoji: str = "🧑‍💻" class CreateMeetupRequest(BaseModel): title: str = Field(min_length=4, max_length=120) city: str = "线上" destination_slug: str = "" emoji: str = "🎉" date: str = "" time: str = "19:00" venue: str = "" description: str = "" mode: str = "offline" access_level: str = "public" max_attendees: int = 30 tags: list[str] = Field(default_factory=list) mirotalkRoom: str = "" loungeChannel: str = "" class GigItem(BaseModel): id: str title: str description: str budget: str deadline: str tags: list[str] = [] poster: str = "" status: str = "open" class GigApplyRequest(BaseModel): message: str = Field(min_length=4, max_length=2000) class NotificationItem(BaseModel): id: str title: str body: str = "" link: str = "" read: bool = False archived: bool = False pinned: bool = False category: str = "general" created_at: str = "" class CreateGigRequest(BaseModel): title: str = Field(min_length=4, max_length=120) description: str = Field(min_length=10, max_length=4000) budget: str = "面议" deadline: str = "" tags: list[str] = Field(default_factory=list) class ServiceItem(BaseModel): id: str title: str description: str emoji: str = "💼" price: str = "" category: str = "" provider: str = "" class ServiceLeadRequest(BaseModel): service_id: str name: str = Field(min_length=2, max_length=80) email: EmailStr message: str = Field(min_length=4, max_length=2000) class VideoItem(BaseModel): id: str slug: str title: str excerpt: str = "" emoji: str = "🎬" duration: str = "" guest: str = "" city: str = "" published_at: str = "" tags: list[str] = Field(default_factory=list) video_url: str = "" class AiAssistantRequest(BaseModel): message: str = Field(min_length=2, max_length=500) class AiAssistantResponse(BaseModel): reply: str cities: list[dict] = Field(default_factory=list) class NewsletterRequest(BaseModel): email: EmailStr source: str = "site" class ContentSubmissionRequest(BaseModel): name: str = Field(min_length=2, max_length=80) title: str = Field(min_length=4, max_length=200) content_type: str = "article" url: str = "" notes: str = "" class VolunteerRequest(BaseModel): name: str = Field(min_length=2, max_length=80) email: EmailStr message: str = Field(min_length=4, max_length=2000) class ReportRequest(BaseModel): target_type: str target_id: str reason: str = Field(min_length=4, max_length=1000) class GoogleLoginRequest(BaseModel): id_token: str class FeedbackRequest(BaseModel): content: str = Field(min_length=4, max_length=4000) category: str = "general" name: str = "" class StatsOverview(BaseModel): meetups: int = 0 discussions: int = 0 gigs: int = 0 members_active: int = 0