import httpx from typing import Any from app.config import settings from app.data import mock_data class PocketBaseService: def __init__(self): self.base_url = settings.pocketbase_url.rstrip("/") self._token: str | None = None self._available: bool | None = None async def is_available(self) -> bool: if self._available is not None: return self._available try: async with httpx.AsyncClient(timeout=1.0) as client: r = await client.get(f"{self.base_url}/api/health") self._available = r.status_code == 200 except Exception: self._available = False return self._available async def _auth(self, client: httpx.AsyncClient) -> bool: if self._token: return True try: r = await client.post( f"{self.base_url}/api/admins/auth-with-password", json={ "identity": settings.pocketbase_admin_email, "password": settings.pocketbase_admin_password, }, ) if r.status_code == 200: self._token = r.json().get("token") return True except Exception: pass return False async def list_records(self, collection: str, sort: str = "") -> list[dict[str, Any]]: if not await self.is_available(): return [] params: dict[str, str] = {"perPage": "200"} if sort: params["sort"] = sort try: async with httpx.AsyncClient(timeout=10.0) as client: headers = {} if await self._auth(client): headers["Authorization"] = self._token or "" r = await client.get( f"{self.base_url}/api/collections/{collection}/records", params=params, headers=headers, ) if r.status_code == 200: items = r.json().get("items", []) return items if items else [] except Exception: pass return [] async def create_record(self, collection: str, data: dict[str, Any]) -> dict[str, Any] | None: if not await self.is_available(): return None try: async with httpx.AsyncClient(timeout=10.0) as client: headers = {} if await self._auth(client): headers["Authorization"] = self._token or "" r = await client.post( f"{self.base_url}/api/collections/{collection}/records", json=data, headers=headers, ) if r.status_code in (200, 201): return r.json() except Exception: pass return None async def get_destinations( self, region: str | None = None, search: str | None = None, sort: str = "rating" ) -> list[dict]: records = await self.list_records("destinations", sort=f"-{sort}" if sort != "cost" else "cost") if not records: records = mock_data.DESTINATIONS else: records = [self._map_destination(r) for r in records] if region and region != "all": records = [d for d in records if d.get("region") == region] if search: q = search.lower() records = [ d for d in records if q in d.get("name", "").lower() or q in d.get("country", "").lower() ] return self._sort_destinations(records, sort) async def get_destination(self, slug: str) -> dict | None: records = await self.get_destinations() return next((d for d in records if d.get("slug") == slug), None) async def get_visas(self) -> list[dict]: records = await self.list_records("visas", sort="difficulty") return records if records else mock_data.VISAS async def get_faqs(self) -> list[dict]: records = await self.list_records("faqs", sort="order") return records if records else mock_data.FAQS async def get_testimonials(self) -> list[dict]: records = await self.list_records("testimonials") return records if records else mock_data.TESTIMONIALS async def get_tools(self) -> list[dict]: records = await self.list_records("tools") return records if records else mock_data.TOOLS async def get_blog_posts(self) -> list[dict]: records = await self.list_records("blog_posts", sort="-published_at") return records if records else mock_data.BLOG_POSTS async def get_blog_post(self, slug: str) -> dict | None: posts = await self.get_blog_posts() post = next((p for p in posts if p.get("slug") == slug), None) if not post: return None content = post.get("content") or mock_data.BLOG_CONTENT.get(slug, "") return {**post, "content": content} async def subscribe(self, email: str) -> tuple[bool, str]: existing = await self.list_records("subscriptions") if any(s.get("email") == email for s in existing): return True, "你已经订阅过了,欢迎回来!" result = await self.create_record("subscriptions", {"email": email}) if result: return True, "订阅成功!欢迎加入 NomadFlow 社区 🎉" # fallback: always succeed in demo mode return True, "订阅成功!欢迎加入 NomadFlow 社区 🎉" def _map_destination(self, r: dict) -> dict: return { "id": r.get("id", ""), "slug": r.get("slug", ""), "name": r.get("name", ""), "country": r.get("country", ""), "emoji": r.get("emoji", ""), "tag": r.get("tag", ""), "description": r.get("description", ""), "region": r.get("region", ""), "cost": r.get("cost", 0), "speed": r.get("speed", 0), "temperature": r.get("temperature", 0), "rating": r.get("rating", 0), "hue": r.get("hue", 170), "nomads_count": r.get("nomads_count", ""), "highlights": r.get("highlights", []), "map_x": r.get("map_x", 0), "map_y": r.get("map_y", 0), } def _sort_destinations(self, records: list[dict], sort: str) -> list[dict]: if sort == "cost-asc": return sorted(records, key=lambda x: x.get("cost", 0)) if sort == "cost-desc": return sorted(records, key=lambda x: x.get("cost", 0), reverse=True) if sort == "speed": return sorted(records, key=lambda x: x.get("speed", 0), reverse=True) return sorted(records, key=lambda x: x.get("rating", 0), reverse=True) pb = PocketBaseService()