nomadweb/backend/app/routers/community.py
eric e23cf9a03c Close Explore/Grow loops: visa wizard, jobs, lessons, gigs notify, map.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-04 09:26:56 -05:00

171 lines
5.9 KiB
Python

"""Community mutations — discussions, meetups, gigs, notifications."""
from fastapi import APIRouter, Header, HTTPException, Query
from app.schemas import (
CreateDiscussionRequest, CreateGigRequest, CreateMeetupRequest, CreateReplyRequest,
FeedbackRequest, GigApplyRequest, GigItem, NotificationItem, StatsOverview,
)
from app.services.auth import get_user_by_token
from app.services import community_store, social_store
router = APIRouter(tags=["community"])
def _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("/discussions")
async def create_discussion(body: CreateDiscussionRequest, authorization: str | None = Header(None)):
user = _user(authorization)
d = community_store.create_discussion(user["id"], user["name"], body.model_dump())
return {"success": True, "discussion": d}
@router.post("/discussions/{discussion_id}/replies")
async def post_reply(
discussion_id: str,
body: CreateReplyRequest,
authorization: str | None = Header(None),
):
user = _user(authorization)
reply = community_store.add_reply(discussion_id, user["id"], user["name"], body.content, body.author_emoji)
if not reply:
raise HTTPException(404, "讨论不存在")
discussion = community_store.get_discussion(discussion_id, increment_view=False) or {}
author_id = discussion.get("author_id") or ""
if author_id and author_id != user["id"]:
from app.services.notify import notify_user
notify_user(
author_id,
"讨论有新回复",
f"{user['name']} 回复了「{discussion.get('title', '话题')}」",
f"/community/{discussion_id}",
"community",
)
return {"success": True, "reply": reply}
@router.post("/discussions/{discussion_id}/like")
async def like_discussion(discussion_id: str, authorization: str | None = Header(None)):
user = _user(authorization)
return community_store.toggle_discussion_like(discussion_id, user["id"])
@router.post("/discussions/{discussion_id}/pin")
async def pin_discussion(discussion_id: str, authorization: str | None = Header(None)):
user = _user(authorization)
d = community_store.toggle_pin(discussion_id, user["id"])
if not d:
raise HTTPException(403, "仅作者可置顶")
return {"success": True, "discussion": d}
@router.post("/meetups")
async def create_meetup(body: CreateMeetupRequest, authorization: str | None = Header(None)):
user = _user(authorization)
m = community_store.create_meetup(user["id"], user["name"], body.model_dump())
return {"success": True, "meetup": m}
@router.get("/meetups/my-rsvp")
async def my_rsvps(authorization: str | None = Header(None)):
user = _user(authorization)
return {"ids": list(community_store.user_rsvp_ids(user["id"]))}
@router.get("/gigs", response_model=list[GigItem])
async def list_gigs():
return [GigItem(**g) for g in community_store.list_gigs()]
@router.post("/gigs")
async def create_gig(body: CreateGigRequest, authorization: str | None = Header(None)):
user = _user(authorization)
g = community_store.create_gig(user["id"], user["name"], body.model_dump())
return {"success": True, "gig": g}
@router.post("/gigs/{gig_id}/apply")
async def apply_gig(gig_id: str, body: GigApplyRequest, authorization: str | None = Header(None)):
user = _user(authorization)
res = community_store.apply_gig(gig_id, user["id"], user["name"], body.message)
if not res.get("ok"):
raise HTTPException(400, res.get("error", "申请失败"))
poster_id = res.get("poster_id") or ""
if not poster_id:
email = (res.get("poster_email") or "").strip().lower()
if email:
from app.services.auth import find_user_public_by_email
peer = find_user_public_by_email(email)
if peer:
poster_id = peer["id"]
if poster_id and poster_id != user["id"]:
from app.services.notify import notify_user
notify_user(
poster_id,
"赏金任务有新申请",
f"{user['name']} 申请了「{res.get('title') or '任务'}」",
"/gigs",
"gig",
)
return {"ok": True, "message": res.get("message", "申请已提交")}
@router.get("/notifications", response_model=list[NotificationItem])
async def list_notifications(authorization: str | None = Header(None)):
user = _user(authorization)
return [NotificationItem(**n) for n in community_store.list_notifications(user["id"])]
@router.post("/notifications/read")
async def mark_read(authorization: str | None = Header(None)):
user = _user(authorization)
community_store.mark_notifications_read(user["id"])
return {"success": True}
@router.post("/notifications/{notif_id}/{action}")
async def notification_action(notif_id: str, action: str, authorization: str | None = Header(None)):
user = _user(authorization)
if action not in ("read", "unread", "archive", "restore", "pin", "unpin"):
raise HTTPException(400, "无效操作")
n = community_store.update_notification(user["id"], notif_id, action)
if not n:
raise HTTPException(404, "通知不存在")
return {"success": True, "notification": n}
@router.post("/feedback")
async def submit_feedback(body: FeedbackRequest, authorization: str | None = Header(None)):
user = None
name = body.name or "访客"
if authorization and authorization.startswith("Bearer "):
user = get_user_by_token(authorization[7:])
if user:
name = user["name"]
item = community_store.add_feedback(user["id"] if user else None, name, body.content, body.category)
return {"success": True, "id": item["id"]}
@router.get("/stats/overview", response_model=StatsOverview)
async def stats_overview():
return StatsOverview(**community_store.stats_overview())
@router.get("/members/{user_id}")
async def public_member(user_id: str):
profile = social_store.get_public_profile(user_id)
if not profile:
raise HTTPException(404, "用户不存在")
return profile