Port meetups, discussions, gigs, dating/chat, VIP pay (ZPay/XorPay), MiroTalk live, digital academy, and ebook reader. Add persistent community_store, git-first deploy docs, and env template for production secrets. Co-authored-by: Cursor <cursoragent@cursor.com>
472 lines
8.9 KiB
Python
472 lines
8.9 KiB
Python
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
|
|
created_at: 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 |