nomadweb/backend/app/routers/api.py
2026-08-28 19:49:10 -05:00

216 lines
6.4 KiB
Python

from fastapi import APIRouter, HTTPException, Query
from app.schemas import (
BlogPost, BlogPostDetail, ChartData, CompareRequest, CostCalculatorRequest,
CostCalculatorResponse, Destination, FAQ, SearchResult, StatsResponse,
SubscribeRequest, SubscribeResponse, Testimonial, Tool, Visa,
)
from app.services.pocketbase import pb
from app.data import mock_data
router = APIRouter()
@router.get("/health")
async def health():
pb_ok = await pb.is_available()
return {"status": "ok", "pocketbase": "connected" if pb_ok else "fallback"}
@router.get("/stats", response_model=StatsResponse)
async def get_stats():
dests = await pb.get_destinations()
avg = sum(d["cost"] for d in dests) // len(dests) if dests else 42
return StatsResponse(
countries=195,
avg_cost=avg // 100,
satisfaction=87,
total_nomads="35.6M",
active_today=127,
)
@router.get("/ticker")
async def get_ticker():
return {"messages": mock_data.TICKER_MESSAGES}
@router.get("/destinations", response_model=list[Destination])
async def list_destinations(
region: str | None = Query(None),
search: str | None = Query(None),
sort: str = Query("rating"),
):
data = await pb.get_destinations(region=region, search=search, sort=sort)
return [Destination(**d) for d in data]
@router.get("/destinations/{slug}", response_model=Destination)
async def get_destination(slug: str):
data = await pb.get_destination(slug)
if not data:
raise HTTPException(404, "目的地不存在")
return Destination(**data)
@router.post("/destinations/compare", response_model=list[Destination])
async def compare_destinations(body: CompareRequest):
all_dests = await pb.get_destinations()
result = [d for d in all_dests if d["slug"] in body.slugs]
if len(result) < 2:
raise HTTPException(400, "至少需要 2 个有效目的地")
return [Destination(**d) for d in result]
@router.get("/visas", response_model=list[Visa])
async def list_visas():
return [Visa(**v) for v in await pb.get_visas()]
@router.get("/faqs", response_model=list[FAQ])
async def list_faqs():
return [FAQ(**f) for f in await pb.get_faqs()]
@router.get("/testimonials", response_model=list[Testimonial])
async def list_testimonials():
return [Testimonial(**t) for t in await pb.get_testimonials()]
@router.get("/tools", response_model=list[Tool])
async def list_tools():
return [Tool(**t) for t in await pb.get_tools()]
@router.get("/blog", response_model=list[BlogPost])
async def list_blog():
return [BlogPost(**b) for b in await pb.get_blog_posts()]
@router.get("/blog/{slug}", response_model=BlogPostDetail)
async def get_blog(slug: str):
post = await pb.get_blog_post(slug)
if not post:
raise HTTPException(404, "文章不存在")
return BlogPostDetail(**post)
@router.post("/subscribe", response_model=SubscribeResponse)
async def subscribe(body: SubscribeRequest):
ok, msg = await pb.subscribe(body.email)
return SubscribeResponse(success=ok, message=msg)
@router.post("/calculator", response_model=CostCalculatorResponse)
async def cost_calculator(body: CostCalculatorRequest):
dest = await pb.get_destination(body.destination_slug)
if not dest:
raise HTTPException(404, "目的地不存在")
multipliers = {"budget": 0.7, "mid": 1.0, "premium": 1.5}
m = multipliers.get(body.housing, 1.0)
base = dest["cost"]
breakdown = {
"🏠 住宿": int(base * 0.35 * m),
"🍜 餐饮": int(base * 0.25 * m),
"🚗 交通": int(base * 0.10),
"📶 网络/办公": int(base * 0.10),
"🎉 娱乐社交": int(base * 0.12 * m),
"🏥 保险医疗": int(base * 0.08),
}
per_month = sum(breakdown.values())
total = per_month * body.months
return CostCalculatorResponse(
destination=f"{dest['name']}, {dest['country']}",
emoji=dest["emoji"],
months=body.months,
breakdown=breakdown,
total=total,
per_month=per_month,
)
@router.get("/charts/cost", response_model=ChartData)
async def chart_cost():
dests = await pb.get_destinations()
dests = sorted(dests, key=lambda x: x["cost"])
return ChartData(
labels=[f"{d['name']} {d['emoji']}" for d in dests],
datasets=[{
"label": "月生活费 (元)",
"data": [d["cost"] for d in dests],
}],
)
@router.get("/charts/speed", response_model=ChartData)
async def chart_speed():
dests = await pb.get_destinations()
dests = sorted(dests, key=lambda x: x["speed"], reverse=True)
return ChartData(
labels=[f"{d['name']} {d['speed']}Mbps" for d in dests],
datasets=[{"data": [d["speed"] for d in dests]}],
)
@router.get("/charts/growth", response_model=ChartData)
async def chart_growth():
return ChartData(
labels=["2019", "2020", "2021", "2022", "2023", "2024", "2025", "2026"],
datasets=[{
"label": "全球数字游民 (百万人)",
"data": [7.3, 10.9, 15.5, 24.0, 28.5, 31.2, 33.8, 35.6],
}],
)
@router.get("/charts/radar", response_model=ChartData)
async def chart_radar():
return ChartData(
labels=["生活成本", "网络速度", "安全性", "社群活跃", "气候环境", "签证便利"],
datasets=[
{"label": "清迈", "data": [95, 80, 85, 95, 70, 90]},
{"label": "里斯本", "data": [60, 90, 92, 80, 85, 95]},
],
)
@router.get("/search", response_model=list[SearchResult])
async def global_search(q: str = Query(..., min_length=1)):
query = q.lower().strip()
results: list[SearchResult] = []
for d in await pb.get_destinations(search=query):
results.append(SearchResult(
type="destination",
title=f"{d['name']}, {d['country']}",
subtitle=f"¥{d['cost']}/月 · ⭐{d['rating']}",
emoji=d["emoji"],
url=f"/destinations/{d['slug']}",
))
for b in await pb.get_blog_posts():
if query in b["title"].lower() or query in b.get("excerpt", "").lower():
results.append(SearchResult(
type="blog", title=b["title"], subtitle=b["excerpt"][:60],
emoji=b["emoji"], url=f"/blog/{b['slug']}",
))
for v in await pb.get_visas():
if query in v["name"].lower() or query in v["country"].lower():
results.append(SearchResult(
type="visa", title=v["name"], subtitle=v["country"],
emoji=v["flag"], url="/#visa",
))
for f in await pb.get_faqs():
if query in f["question"].lower() or query in f["answer"].lower():
results.append(SearchResult(
type="faq", title=f["question"], subtitle=f["answer"][:60],
emoji="❓", url="/#faq",
))
return results[:12]