nomadweb/backend/app/routers/platform.py
eric 48408d3079 feat: complete NomadCNA parity layer with platform APIs and pages
Add services, videos, AI assistant, map/weather, gigs post, notifications settings, static legal pages, Google OAuth, payment router, real-user matching, newsletter, and content submission — all using nomadweb UI patterns.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-30 10:13:17 -05:00

186 lines
5.5 KiB
Python

"""Platform APIs: services, videos, weather, AI, stats, content."""
from fastapi import APIRouter, Header, HTTPException, Query
from app.schemas import (
AiAssistantRequest, AiAssistantResponse, ContentSubmissionRequest,
NewsletterRequest, ReportRequest, ServiceItem, ServiceLeadRequest,
VideoItem, VolunteerRequest,
)
from app.data import platform_data
from app.services import community_store, social_store
from app.services.auth import get_user_by_token
from app.services import platform_store
from app.services.weather_service import get_city_weather, list_weather
from app.services.ai_service import assistant_reply
from app.services.pocketbase import pb
router = APIRouter(tags=["platform"])
def _optional_user(authorization: str | None) -> dict | None:
if authorization and authorization.startswith("Bearer "):
return get_user_by_token(authorization[7:])
return None
def _user(authorization: str | None) -> dict:
u = _optional_user(authorization)
if not u:
raise HTTPException(401, "未登录")
return u
@router.get("/services", response_model=list[ServiceItem])
async def list_services():
return [ServiceItem(**s) for s in platform_data.SERVICES]
@router.post("/services/leads")
async def service_lead(body: ServiceLeadRequest, authorization: str | None = Header(None)):
user = _optional_user(authorization)
item = platform_store.add_service_lead(
body.service_id,
user["id"] if user else None,
body.name,
body.email,
body.message,
)
return {"success": True, "id": item["id"]}
@router.get("/videos", response_model=list[VideoItem])
async def list_videos():
return [VideoItem(**v) for v in platform_data.VIDEOS]
@router.get("/videos/{slug}", response_model=VideoItem)
async def get_video(slug: str):
v = next((x for x in platform_data.VIDEOS if x["slug"] == slug), None)
if not v:
raise HTTPException(404, "视频不存在")
return VideoItem(**v)
@router.get("/content/home")
async def content_home():
return {
"featured": platform_data.VIDEOS[:2],
"videos_count": len(platform_data.VIDEOS),
"services_count": len(platform_data.SERVICES),
}
@router.post("/content/submissions")
async def content_submission(body: ContentSubmissionRequest, authorization: str | None = Header(None)):
user = _optional_user(authorization)
item = platform_store.add_content_submission(
user["id"] if user else None,
body.name,
body.title,
body.content_type,
body.url,
body.notes,
)
return {"success": True, "id": item["id"]}
@router.get("/weather/cities")
async def weather_cities():
return await list_weather()
@router.get("/weather/cities/{slug}")
async def weather_city(slug: str):
w = await get_city_weather(slug)
if not w:
raise HTTPException(404, "城市不存在")
return w
@router.post("/ai/assistant", response_model=AiAssistantResponse)
async def ai_assistant(body: AiAssistantRequest):
dests = await pb.get_destinations()
result = assistant_reply(body.message, dests)
return AiAssistantResponse(**result)
@router.post("/newsletter/subscribe")
async def newsletter_subscribe(body: NewsletterRequest):
res = platform_store.subscribe_newsletter(body.email, body.source)
return res
@router.get("/stats/member-map")
async def member_map():
return {"members": platform_data.MEMBER_MAP}
@router.get("/stats/city-ranking")
async def city_ranking():
return {"items": platform_data.CITY_RANKING}
@router.post("/cities/{slug}/volunteer-applications")
async def volunteer_apply(slug: str, body: VolunteerRequest, authorization: str | None = Header(None)):
user = _optional_user(authorization)
item = platform_store.add_volunteer(slug, user["id"] if user else None, body.name, body.email, body.message)
return {"success": True, "id": item["id"]}
@router.post("/reports")
async def submit_report(body: ReportRequest, authorization: str | None = Header(None)):
user = _optional_user(authorization)
item = platform_store.add_report(
user["id"] if user else None,
body.target_type,
body.target_id,
body.reason,
)
return {"success": True, "id": item["id"]}
@router.get("/digital/day/{day_id}")
async def digital_day(day_id: str):
tip = platform_data.DAILY_TIPS.get(day_id)
if not tip:
raise HTTPException(404, "每日指南不存在")
return {"id": day_id, **tip}
@router.get("/notifications/unread-count")
async def unread_count(authorization: str | None = Header(None)):
user = _user(authorization)
return {"count": community_store.notification_unread_count(user["id"])}
@router.get("/notifications/preferences")
async def get_notif_prefs(authorization: str | None = Header(None)):
user = _user(authorization)
return platform_store.get_notif_prefs(user["id"])
@router.put("/notifications/preferences")
async def set_notif_prefs(body: dict, authorization: str | None = Header(None)):
user = _user(authorization)
return platform_store.set_notif_prefs(user["id"], body)
@router.get("/discussions/search")
async def search_discussions(q: str = Query(..., min_length=1)):
return community_store.search_discussions(q)
@router.get("/meetups/{meetup_id}/social-suggestions")
async def meetup_social(meetup_id: str):
return {"items": community_store.meetup_social_suggestions(meetup_id)}
@router.delete("/meetups/{meetup_id}/rsvp")
async def cancel_rsvp(meetup_id: str, authorization: str | None = Header(None)):
user = _user(authorization)
res = community_store.cancel_rsvp(meetup_id, user["id"])
if not res.get("ok"):
raise HTTPException(400, "未报名此活动")
return res