439 lines
18 KiB
Python
439 lines
18 KiB
Python
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", "<br>")
|
|
open_hint = "请点击右上角,选择“打开小程序”或使用活动二维码进入。"
|
|
html = f"""<!doctype html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
|
<title>{safe_title}</title>
|
|
<style>
|
|
body {{ margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: linear-gradient(180deg, #f7f1e8 0%, #fffdf8 100%); color: #1f2937; }}
|
|
.wrap {{ max-width: 720px; margin: 0 auto; padding: 28px 20px 40px; }}
|
|
.card {{ background: rgba(255,255,255,0.92); border-radius: 24px; overflow: hidden; box-shadow: 0 18px 50px rgba(15,23,42,0.12); }}
|
|
.cover {{ width: 100%; aspect-ratio: 16 / 9; object-fit: cover; background: #e5e7eb; display: block; }}
|
|
.body {{ padding: 24px; }}
|
|
h1 {{ margin: 0 0 12px; font-size: 28px; line-height: 1.2; }}
|
|
p {{ margin: 0; color: #4b5563; line-height: 1.7; }}
|
|
.actions {{ display: flex; flex-wrap: wrap; gap: 12px; margin-top: 24px; }}
|
|
.btn {{ display: inline-flex; align-items: center; justify-content: center; padding: 14px 18px; border-radius: 999px; text-decoration: none; font-weight: 600; }}
|
|
.btn-primary {{ background: #111827; color: #fff; }}
|
|
.btn-secondary {{ background: #f3f4f6; color: #111827; }}
|
|
.hint {{ margin-top: 18px; font-size: 14px; color: #6b7280; }}
|
|
.path {{ margin-top: 14px; padding: 12px 14px; border-radius: 14px; background: #f8fafc; font-size: 13px; color: #475569; word-break: break-all; }}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="wrap">
|
|
<div class="card">
|
|
{"<img class='cover' src='" + cover_image + "' alt='活动封面'>" if cover_image else "<div class='cover'></div>"}
|
|
<div class="body">
|
|
<h1>{safe_title}</h1>
|
|
<p>{safe_description}</p>
|
|
<div class="actions">
|
|
<a class="btn btn-primary" href="{url_link or 'weixin://'}">打开小程序</a>
|
|
<a class="btn btn-secondary" href="/static/uploads/activities/{activity.id}/share_qrcode.png" target="_blank" rel="noreferrer">查看活动二维码</a>
|
|
</div>
|
|
<div class="hint">{open_hint}</div>
|
|
<div class="path">小程序页面路径:{share_path}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>"""
|
|
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()}
|