from fastapi import APIRouter, Depends, HTTPException, Query, status from fastapi.responses import HTMLResponse from sqlalchemy.ext.asyncio import AsyncSession from app.database import get_db from app.dependencies import get_current_user_id, get_optional_current_user_id from app.schemas.activity import ActivityDetailResponse, ActivityListItem, ActivityRegisterResponse from app.schemas.activity_match import ( ActivityCandidateItem, ActivityChoiceActionResponse, ActivityChoiceRequest, ActivityMatchDetailResponse, ActivityMatchListItem, ActivityMatchStateResponse, ) from app.services.activity_match_service import ( bind_activity_user, build_activity_share_link, build_activity_share_path, build_activity_url_link, build_activity_share_info, cancel_activity_choice, create_activity_choice, get_activity_by_share_token, get_activity_match_detail, get_activity_match_state, list_activity_candidates, list_activity_liked_me, list_activity_mutual_matches, list_my_activity_choices, ) from app.services.activity_service import ( cancel_registration, get_activity_by_id, get_activity_detail, list_activities, list_my_activities, register_activity, ) from app.services.user_service import get_user_by_id router = APIRouter() @router.get("") async def get_activities( status_filter: int | None = Query(default=None, alias="status"), page: int = Query(default=1, ge=1), page_size: int = Query(default=10, ge=1, le=50), current_user_id: int = Depends(get_current_user_id), session: AsyncSession = Depends(get_db), ) -> dict: current_user = await get_user_by_id(session, current_user_id) items = await list_activities( session, current_user, status=status_filter, page=page, page_size=page_size, ) data = [ActivityListItem(**item).model_dump() for item in items] return {"code": 0, "message": "ok", "data": data} @router.get("/mine") async def get_my_activities( current_user_id: int = Depends(get_current_user_id), session: AsyncSession = Depends(get_db), ) -> dict: current_user = await get_user_by_id(session, current_user_id) if current_user is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") items = await list_my_activities(session, current_user) data = [ActivityListItem(**item).model_dump() for item in items] return {"code": 0, "message": "ok", "data": data} @router.get("/share/{share_token}") async def get_activity_by_share( share_token: str, current_user_id: int | None = Depends(get_optional_current_user_id), session: AsyncSession = Depends(get_db), ) -> dict: activity = await get_activity_by_share_token(session, share_token) if activity is None or activity.is_published != 1: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Activity not found") current_user = await get_user_by_id(session, current_user_id) if current_user_id else None data = await build_activity_share_info(session, activity, current_user) return {"code": 0, "message": "ok", "data": data} @router.get("/share/{share_token}/landing", response_class=HTMLResponse, include_in_schema=False) async def activity_share_landing( share_token: str, session: AsyncSession = Depends(get_db), ) -> HTMLResponse: activity = await get_activity_by_share_token(session, share_token) if activity is None or activity.is_published != 1: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Activity not found") share_path = await build_activity_share_path(session, activity) url_link = await build_activity_url_link(session, activity) title = activity.title or "活动报名" description = activity.description or "点击打开小程序查看活动详情并报名" cover_image = activity.cover_image or "" safe_title = title.replace("&", "&").replace("<", "<").replace(">", ">") safe_description = description.replace("&", "&").replace("<", "<").replace(">", ">").replace("\n", "
") open_hint = "请点击右上角,选择“打开小程序”或使用活动二维码进入。" html = f""" {safe_title}
{"活动封面" if cover_image else "
"}

{safe_title}

{safe_description}

{open_hint}
小程序页面路径:{share_path}
""" return HTMLResponse(content=html) @router.get("/{activity_id}") async def get_activity( activity_id: int, current_user_id: int = Depends(get_current_user_id), session: AsyncSession = Depends(get_db), ) -> dict: activity = await get_activity_by_id(session, activity_id) if activity is None or activity.is_published != 1: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Activity not found") current_user = await get_user_by_id(session, current_user_id) detail = await get_activity_detail(session, activity, current_user) return {"code": 0, "message": "ok", "data": ActivityDetailResponse(**detail).model_dump()} @router.post("/{activity_id}/register") async def register( activity_id: int, current_user_id: int = Depends(get_current_user_id), session: AsyncSession = Depends(get_db), ) -> dict: activity = await get_activity_by_id(session, activity_id) if activity is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Activity not found") current_user = await get_user_by_id(session, current_user_id) if current_user is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") try: registration = await register_activity(session, activity, current_user) except ValueError as exc: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc data = ActivityRegisterResponse( registration_id=registration.id, status=registration.status, message="报名成功", ).model_dump() return {"code": 0, "message": "ok", "data": data} @router.post("/{activity_id}/bind") async def bind_activity( activity_id: int, current_user_id: int = Depends(get_current_user_id), session: AsyncSession = Depends(get_db), ) -> dict: activity = await get_activity_by_id(session, activity_id) if activity is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Activity not found") current_user = await get_user_by_id(session, current_user_id) if current_user is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") try: registration = await bind_activity_user(session, activity, current_user) except ValueError as exc: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc data = ActivityRegisterResponse( registration_id=registration.id, status=registration.status, message="绑定成功", ).model_dump() return {"code": 0, "message": "ok", "data": data} @router.delete("/{activity_id}/register") async def cancel_register( activity_id: int, current_user_id: int = Depends(get_current_user_id), session: AsyncSession = Depends(get_db), ) -> dict: activity = await get_activity_by_id(session, activity_id) if activity is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Activity not found") current_user = await get_user_by_id(session, current_user_id) if current_user is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") try: registration = await cancel_registration(session, activity, current_user) except ValueError as exc: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc data = ActivityRegisterResponse( registration_id=registration.id, status=registration.status, message="报名已取消", ).model_dump() return {"code": 0, "message": "ok", "data": data} @router.get("/{activity_id}/match-state") async def activity_match_state( activity_id: int, current_user_id: int = Depends(get_current_user_id), session: AsyncSession = Depends(get_db), ) -> dict: activity = await get_activity_by_id(session, activity_id) if activity is None or activity.is_published != 1: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Activity not found") current_user = await get_user_by_id(session, current_user_id) if current_user is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") try: data = await get_activity_match_state(session, activity, current_user) except PermissionError as exc: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=str(exc)) from exc return {"code": 0, "message": "ok", "data": ActivityMatchStateResponse(**data).model_dump()} @router.get("/{activity_id}/candidates") async def activity_candidates( activity_id: int, current_user_id: int = Depends(get_current_user_id), session: AsyncSession = Depends(get_db), ) -> dict: activity = await get_activity_by_id(session, activity_id) if activity is None or activity.is_published != 1: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Activity not found") current_user = await get_user_by_id(session, current_user_id) if current_user is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") try: items = await list_activity_candidates(session, activity, current_user) except PermissionError as exc: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=str(exc)) from exc data = [ActivityCandidateItem(**item).model_dump() for item in items] return {"code": 0, "message": "ok", "data": data} @router.post("/{activity_id}/choices") async def create_choice( activity_id: int, payload: ActivityChoiceRequest, current_user_id: int = Depends(get_current_user_id), session: AsyncSession = Depends(get_db), ) -> dict: activity = await get_activity_by_id(session, activity_id) if activity is None or activity.is_published != 1: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Activity not found") current_user = await get_user_by_id(session, current_user_id) target_user = await get_user_by_id(session, payload.to_user_id) if current_user is None or target_user is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") try: result = await create_activity_choice(session, activity, current_user, target_user) except PermissionError as exc: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=str(exc)) from exc except ValueError as exc: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc return {"code": 0, "message": "ok", "data": ActivityChoiceActionResponse(**result).model_dump()} @router.delete("/{activity_id}/choices/{target_user_id}") async def delete_choice( activity_id: int, target_user_id: int, current_user_id: int = Depends(get_current_user_id), session: AsyncSession = Depends(get_db), ) -> dict: activity = await get_activity_by_id(session, activity_id) if activity is None or activity.is_published != 1: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Activity not found") current_user = await get_user_by_id(session, current_user_id) if current_user is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") try: result = await cancel_activity_choice(session, activity, current_user, target_user_id) except PermissionError as exc: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=str(exc)) from exc except ValueError as exc: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc return {"code": 0, "message": "ok", "data": ActivityChoiceActionResponse(**result).model_dump()} @router.get("/{activity_id}/my-choices") async def my_choices( activity_id: int, current_user_id: int = Depends(get_current_user_id), session: AsyncSession = Depends(get_db), ) -> dict: activity = await get_activity_by_id(session, activity_id) if activity is None or activity.is_published != 1: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Activity not found") current_user = await get_user_by_id(session, current_user_id) if current_user is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") try: items = await list_my_activity_choices(session, activity, current_user) except PermissionError as exc: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=str(exc)) from exc data = [ActivityMatchListItem(**item).model_dump() for item in items] return {"code": 0, "message": "ok", "data": data} @router.get("/{activity_id}/liked-me") async def liked_me( activity_id: int, current_user_id: int = Depends(get_current_user_id), session: AsyncSession = Depends(get_db), ) -> dict: activity = await get_activity_by_id(session, activity_id) if activity is None or activity.is_published != 1: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Activity not found") current_user = await get_user_by_id(session, current_user_id) if current_user is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") try: items = await list_activity_liked_me(session, activity, current_user) except PermissionError as exc: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=str(exc)) from exc data = [ActivityMatchListItem(**item).model_dump() for item in items] return {"code": 0, "message": "ok", "data": data} @router.get("/{activity_id}/mutual-matches") async def mutual_matches( activity_id: int, current_user_id: int = Depends(get_current_user_id), session: AsyncSession = Depends(get_db), ) -> dict: activity = await get_activity_by_id(session, activity_id) if activity is None or activity.is_published != 1: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Activity not found") current_user = await get_user_by_id(session, current_user_id) if current_user is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") try: items = await list_activity_mutual_matches(session, activity, current_user) except PermissionError as exc: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=str(exc)) from exc data = [ActivityMatchListItem(**item).model_dump() for item in items] return {"code": 0, "message": "ok", "data": data} @router.get("/{activity_id}/matches/{target_user_id}/detail") async def activity_match_detail( activity_id: int, target_user_id: int, current_user_id: int = Depends(get_current_user_id), session: AsyncSession = Depends(get_db), ) -> dict: activity = await get_activity_by_id(session, activity_id) if activity is None or activity.is_published != 1: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Activity not found") current_user = await get_user_by_id(session, current_user_id) if current_user is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") try: detail = await get_activity_match_detail(session, activity, current_user, target_user_id) except PermissionError as exc: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=str(exc)) from exc if detail is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Match not found") return {"code": 0, "message": "ok", "data": ActivityMatchDetailResponse(**detail).model_dump()}