nomadweb/backend/app/routers/social.py
eric ff7b721acb feat: NomadCNA community layer, social matching, payments, and ebook
Port meetups, discussions, gigs, dating/chat, VIP pay (ZPay/XorPay), MiroTalk live, digital academy, and ebook reader. Add persistent community_store, git-first deploy docs, and env template for production secrets.

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

135 lines
4.9 KiB
Python

"""Matching, chat, join, VIP — adapted from NomadCNA."""
from fastapi import APIRouter, Header, HTTPException, Query
from app.schemas import (
ChatMessage, ConversationItem, JoinMemberRequest, MatchProfile, MatchQuota,
SendMessageRequest, SwipeRequest, SwipeResponse, VipStatus,
)
from app.services.auth import get_user_by_token
from app.services import social_store
router = APIRouter(prefix="/social", tags=["social"])
def _token_user(authorization: str | None) -> dict:
if not authorization or not authorization.startswith("Bearer "):
raise HTTPException(401, "未登录")
user = get_user_by_token(authorization[7:])
if not user:
raise HTTPException(401, "未登录")
return user
@router.post("/join")
async def join_member(body: JoinMemberRequest, authorization: str | None = Header(None)):
user = _token_user(authorization)
profile = social_store.join_member(user["id"], user["name"], body.model_dump())
return {"success": True, "profile": profile}
@router.get("/join/status")
async def join_status(authorization: str | None = Header(None)):
user = _token_user(authorization)
profile = social_store._profiles.get(user["id"])
return {"complete": bool(profile), "profile": profile}
@router.get("/matches/candidates", response_model=list[MatchProfile])
async def match_candidates(
authorization: str | None = Header(None),
intent: str = Query("friends"),
city: str = Query(""),
gender: str = Query(""),
single: str = Query(""),
exclude_swiped: bool = Query(True),
):
user = _token_user(authorization)
if user["id"] not in social_store._profiles:
raise HTTPException(400, "请先完成加入资料")
return [MatchProfile(**p) for p in social_store.list_candidates(
user["id"], intent=intent, city=city, gender=gender, single=single, exclude_swiped=exclude_swiped
)]
@router.get("/matches/quota", response_model=MatchQuota)
async def match_quota(authorization: str | None = Header(None)):
user = _token_user(authorization)
return MatchQuota(**social_store.get_quota(user["id"]))
@router.post("/matches/swipes/undo")
async def undo_swipe(authorization: str | None = Header(None)):
user = _token_user(authorization)
res = social_store.undo_last_swipe(user["id"])
if not res.get("ok"):
raise HTTPException(400, "没有可撤销的滑动")
return res
@router.get("/matches/likes", response_model=list[MatchProfile])
async def match_likes(authorization: str | None = Header(None)):
user = _token_user(authorization)
return [MatchProfile(**p) for p in social_store.list_likes(user["id"])]
@router.get("/matches/mutual")
async def mutual_matches(authorization: str | None = Header(None)):
user = _token_user(authorization)
return {"items": social_store.list_mutual(user["id"])}
@router.post("/matches/swipes", response_model=SwipeResponse)
async def record_swipe(body: SwipeRequest, authorization: str | None = Header(None)):
user = _token_user(authorization)
result = social_store.record_swipe(user["id"], body.profileId, body.action, body.intent)
if not result.get("ok"):
err = result.get("error")
if err == "quota_exceeded":
raise HTTPException(429, "今日滑动次数已用完")
if err == "vip_required":
raise HTTPException(403, "超级喜欢需要 VIP")
if err == "duplicate":
raise HTTPException(409, "已滑动过该用户")
raise HTTPException(400, err or "滑动失败")
return SwipeResponse(**result)
@router.get("/conversations", response_model=list[ConversationItem])
async def list_conversations(authorization: str | None = Header(None)):
user = _token_user(authorization)
items = social_store.list_conversations(user["id"])
return [ConversationItem(**c) for c in items]
@router.get("/conversations/{conv_id}")
async def get_conversation(conv_id: str, authorization: str | None = Header(None)):
user = _token_user(authorization)
conv = social_store.get_conversation(conv_id, user["id"])
if not conv:
raise HTTPException(404, "会话不存在")
return conv
@router.get("/conversations/{conv_id}/messages", response_model=list[ChatMessage])
async def get_messages(conv_id: str, authorization: str | None = Header(None), limit: int = 50):
user = _token_user(authorization)
return [ChatMessage(**m) for m in social_store.list_messages(conv_id, user["id"], limit)]
@router.post("/conversations/{conv_id}/messages", response_model=ChatMessage)
async def post_message(conv_id: str, body: SendMessageRequest, authorization: str | None = Header(None)):
user = _token_user(authorization)
msg = social_store.send_message(conv_id, user["id"], body.body)
if not msg:
raise HTTPException(400, "发送失败")
return ChatMessage(**msg)
@router.get("/vip/check", response_model=VipStatus)
async def vip_check(authorization: str | None = Header(None)):
user = _token_user(authorization)
vip = social_store.is_vip(user["id"])
exp = (social_store._memberships.get(user["id"]) or {}).get("expires_at", 0)
return VipStatus(vip=vip, expires_at=exp)