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()}