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]