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>
186 lines
5.5 KiB
Python
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
|