xiangqinxiaochengxu/backend/app/routers/activities.py

439 lines
18 KiB
Python
Raw Normal View History

2026-04-17 10:49:14 +08:00
from fastapi import APIRouter, Depends, HTTPException, Query, status
2026-05-18 14:24:55 +08:00
from fastapi.responses import HTMLResponse
2026-04-17 10:49:14 +08:00
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
更新功能修改完版本提交: 功能需求:活动分享与嘉宾匹配系统 1. 活动分享入口 每个活动配置独立的分享链接和二维码,支持扫码或点击链接进入小程序。 2. 用户绑定流程 用户通过分享链接/二维码打开小程序,填写个人信息并绑定至对应活动。 若用户曾参与过历史活动,系统自动复用其历史信息并完成绑定,无需重复填写。 绑定成功后,在活动正式开始前,用户无法查看其他参与者的信息。 3. 信息可见性规则 活动开始前:仅允许查看自己的信息,其他用户信息不可见。 活动开始后:开放本活动内异性嘉宾资料查看与匹配,其他活动的用户信息完全隔离,不可见。 资料展示:男嘉宾、女嘉宾资料分开展示,用户仅可浏览异性嘉宾信息。 4. 嘉宾选择规则 每位用户最多可选择 3 位心仪嘉宾。 确认提交后不可再新增选择,但支持修改或撤销已选嘉宾。 管理员可设置匹配截止时间,到达截止时间后系统自动锁定,禁止任何修改。 5. 匹配判定规则 双向互选:若双方恰好互选,则判定为匹配成功,双方均可查看匹配结果。 多向匹配:用户同时与多位嘉宾匹配成功,全部同时展示。 单向选择:A选择B但B未选择A,A可在"谁选了我"页面看到B。 6. 用户页面说明 异性嘉宾列表:展示所有异性嘉宾资料,支持选择/取消选择。 我的选择:展示当前已选的3位嘉宾,支持修改/撤销(截止前)。 谁选了我:展示所有选择了自己的嘉宾(无论自己是否选择了对方)。 匹配成功:展示所有双向互选的匹配对象,支持查看对方完整资料。
2026-05-17 10:23:02 +08:00
from app.dependencies import get_current_user_id, get_optional_current_user_id
2026-04-17 10:49:14 +08:00
from app.schemas.activity import ActivityDetailResponse, ActivityListItem, ActivityRegisterResponse
更新功能修改完版本提交: 功能需求:活动分享与嘉宾匹配系统 1. 活动分享入口 每个活动配置独立的分享链接和二维码,支持扫码或点击链接进入小程序。 2. 用户绑定流程 用户通过分享链接/二维码打开小程序,填写个人信息并绑定至对应活动。 若用户曾参与过历史活动,系统自动复用其历史信息并完成绑定,无需重复填写。 绑定成功后,在活动正式开始前,用户无法查看其他参与者的信息。 3. 信息可见性规则 活动开始前:仅允许查看自己的信息,其他用户信息不可见。 活动开始后:开放本活动内异性嘉宾资料查看与匹配,其他活动的用户信息完全隔离,不可见。 资料展示:男嘉宾、女嘉宾资料分开展示,用户仅可浏览异性嘉宾信息。 4. 嘉宾选择规则 每位用户最多可选择 3 位心仪嘉宾。 确认提交后不可再新增选择,但支持修改或撤销已选嘉宾。 管理员可设置匹配截止时间,到达截止时间后系统自动锁定,禁止任何修改。 5. 匹配判定规则 双向互选:若双方恰好互选,则判定为匹配成功,双方均可查看匹配结果。 多向匹配:用户同时与多位嘉宾匹配成功,全部同时展示。 单向选择:A选择B但B未选择A,A可在"谁选了我"页面看到B。 6. 用户页面说明 异性嘉宾列表:展示所有异性嘉宾资料,支持选择/取消选择。 我的选择:展示当前已选的3位嘉宾,支持修改/撤销(截止前)。 谁选了我:展示所有选择了自己的嘉宾(无论自己是否选择了对方)。 匹配成功:展示所有双向互选的匹配对象,支持查看对方完整资料。
2026-05-17 10:23:02 +08:00
from app.schemas.activity_match import (
ActivityCandidateItem,
ActivityChoiceActionResponse,
ActivityChoiceRequest,
ActivityMatchDetailResponse,
ActivityMatchListItem,
ActivityMatchStateResponse,
)
from app.services.activity_match_service import (
bind_activity_user,
2026-05-18 14:24:55 +08:00
build_activity_share_link,
build_activity_share_path,
build_activity_url_link,
更新功能修改完版本提交: 功能需求:活动分享与嘉宾匹配系统 1. 活动分享入口 每个活动配置独立的分享链接和二维码,支持扫码或点击链接进入小程序。 2. 用户绑定流程 用户通过分享链接/二维码打开小程序,填写个人信息并绑定至对应活动。 若用户曾参与过历史活动,系统自动复用其历史信息并完成绑定,无需重复填写。 绑定成功后,在活动正式开始前,用户无法查看其他参与者的信息。 3. 信息可见性规则 活动开始前:仅允许查看自己的信息,其他用户信息不可见。 活动开始后:开放本活动内异性嘉宾资料查看与匹配,其他活动的用户信息完全隔离,不可见。 资料展示:男嘉宾、女嘉宾资料分开展示,用户仅可浏览异性嘉宾信息。 4. 嘉宾选择规则 每位用户最多可选择 3 位心仪嘉宾。 确认提交后不可再新增选择,但支持修改或撤销已选嘉宾。 管理员可设置匹配截止时间,到达截止时间后系统自动锁定,禁止任何修改。 5. 匹配判定规则 双向互选:若双方恰好互选,则判定为匹配成功,双方均可查看匹配结果。 多向匹配:用户同时与多位嘉宾匹配成功,全部同时展示。 单向选择:A选择B但B未选择A,A可在"谁选了我"页面看到B。 6. 用户页面说明 异性嘉宾列表:展示所有异性嘉宾资料,支持选择/取消选择。 我的选择:展示当前已选的3位嘉宾,支持修改/撤销(截止前)。 谁选了我:展示所有选择了自己的嘉宾(无论自己是否选择了对方)。 匹配成功:展示所有双向互选的匹配对象,支持查看对方完整资料。
2026-05-17 10:23:02 +08:00
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,
)
2026-04-17 10:49:14 +08:00
from app.services.activity_service import (
cancel_registration,
get_activity_by_id,
get_activity_detail,
list_activities,
更新功能修改完版本提交: 功能需求:活动分享与嘉宾匹配系统 1. 活动分享入口 每个活动配置独立的分享链接和二维码,支持扫码或点击链接进入小程序。 2. 用户绑定流程 用户通过分享链接/二维码打开小程序,填写个人信息并绑定至对应活动。 若用户曾参与过历史活动,系统自动复用其历史信息并完成绑定,无需重复填写。 绑定成功后,在活动正式开始前,用户无法查看其他参与者的信息。 3. 信息可见性规则 活动开始前:仅允许查看自己的信息,其他用户信息不可见。 活动开始后:开放本活动内异性嘉宾资料查看与匹配,其他活动的用户信息完全隔离,不可见。 资料展示:男嘉宾、女嘉宾资料分开展示,用户仅可浏览异性嘉宾信息。 4. 嘉宾选择规则 每位用户最多可选择 3 位心仪嘉宾。 确认提交后不可再新增选择,但支持修改或撤销已选嘉宾。 管理员可设置匹配截止时间,到达截止时间后系统自动锁定,禁止任何修改。 5. 匹配判定规则 双向互选:若双方恰好互选,则判定为匹配成功,双方均可查看匹配结果。 多向匹配:用户同时与多位嘉宾匹配成功,全部同时展示。 单向选择:A选择B但B未选择A,A可在"谁选了我"页面看到B。 6. 用户页面说明 异性嘉宾列表:展示所有异性嘉宾资料,支持选择/取消选择。 我的选择:展示当前已选的3位嘉宾,支持修改/撤销(截止前)。 谁选了我:展示所有选择了自己的嘉宾(无论自己是否选择了对方)。 匹配成功:展示所有双向互选的匹配对象,支持查看对方完整资料。
2026-05-17 10:23:02 +08:00
list_my_activities,
2026-04-17 10:49:14 +08:00
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}
更新功能修改完版本提交: 功能需求:活动分享与嘉宾匹配系统 1. 活动分享入口 每个活动配置独立的分享链接和二维码,支持扫码或点击链接进入小程序。 2. 用户绑定流程 用户通过分享链接/二维码打开小程序,填写个人信息并绑定至对应活动。 若用户曾参与过历史活动,系统自动复用其历史信息并完成绑定,无需重复填写。 绑定成功后,在活动正式开始前,用户无法查看其他参与者的信息。 3. 信息可见性规则 活动开始前:仅允许查看自己的信息,其他用户信息不可见。 活动开始后:开放本活动内异性嘉宾资料查看与匹配,其他活动的用户信息完全隔离,不可见。 资料展示:男嘉宾、女嘉宾资料分开展示,用户仅可浏览异性嘉宾信息。 4. 嘉宾选择规则 每位用户最多可选择 3 位心仪嘉宾。 确认提交后不可再新增选择,但支持修改或撤销已选嘉宾。 管理员可设置匹配截止时间,到达截止时间后系统自动锁定,禁止任何修改。 5. 匹配判定规则 双向互选:若双方恰好互选,则判定为匹配成功,双方均可查看匹配结果。 多向匹配:用户同时与多位嘉宾匹配成功,全部同时展示。 单向选择:A选择B但B未选择A,A可在"谁选了我"页面看到B。 6. 用户页面说明 异性嘉宾列表:展示所有异性嘉宾资料,支持选择/取消选择。 我的选择:展示当前已选的3位嘉宾,支持修改/撤销(截止前)。 谁选了我:展示所有选择了自己的嘉宾(无论自己是否选择了对方)。 匹配成功:展示所有双向互选的匹配对象,支持查看对方完整资料。
2026-05-17 10:23:02 +08:00
@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}
2026-05-18 14:24:55 +08:00
@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("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
safe_description = description.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").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)
2026-04-17 10:49:14 +08:00
@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,
更新功能修改完版本提交: 功能需求:活动分享与嘉宾匹配系统 1. 活动分享入口 每个活动配置独立的分享链接和二维码,支持扫码或点击链接进入小程序。 2. 用户绑定流程 用户通过分享链接/二维码打开小程序,填写个人信息并绑定至对应活动。 若用户曾参与过历史活动,系统自动复用其历史信息并完成绑定,无需重复填写。 绑定成功后,在活动正式开始前,用户无法查看其他参与者的信息。 3. 信息可见性规则 活动开始前:仅允许查看自己的信息,其他用户信息不可见。 活动开始后:开放本活动内异性嘉宾资料查看与匹配,其他活动的用户信息完全隔离,不可见。 资料展示:男嘉宾、女嘉宾资料分开展示,用户仅可浏览异性嘉宾信息。 4. 嘉宾选择规则 每位用户最多可选择 3 位心仪嘉宾。 确认提交后不可再新增选择,但支持修改或撤销已选嘉宾。 管理员可设置匹配截止时间,到达截止时间后系统自动锁定,禁止任何修改。 5. 匹配判定规则 双向互选:若双方恰好互选,则判定为匹配成功,双方均可查看匹配结果。 多向匹配:用户同时与多位嘉宾匹配成功,全部同时展示。 单向选择:A选择B但B未选择A,A可在"谁选了我"页面看到B。 6. 用户页面说明 异性嘉宾列表:展示所有异性嘉宾资料,支持选择/取消选择。 我的选择:展示当前已选的3位嘉宾,支持修改/撤销(截止前)。 谁选了我:展示所有选择了自己的嘉宾(无论自己是否选择了对方)。 匹配成功:展示所有双向互选的匹配对象,支持查看对方完整资料。
2026-05-17 10:23:02 +08:00
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="绑定成功",
2026-04-17 10:49:14 +08:00
).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}
更新功能修改完版本提交: 功能需求:活动分享与嘉宾匹配系统 1. 活动分享入口 每个活动配置独立的分享链接和二维码,支持扫码或点击链接进入小程序。 2. 用户绑定流程 用户通过分享链接/二维码打开小程序,填写个人信息并绑定至对应活动。 若用户曾参与过历史活动,系统自动复用其历史信息并完成绑定,无需重复填写。 绑定成功后,在活动正式开始前,用户无法查看其他参与者的信息。 3. 信息可见性规则 活动开始前:仅允许查看自己的信息,其他用户信息不可见。 活动开始后:开放本活动内异性嘉宾资料查看与匹配,其他活动的用户信息完全隔离,不可见。 资料展示:男嘉宾、女嘉宾资料分开展示,用户仅可浏览异性嘉宾信息。 4. 嘉宾选择规则 每位用户最多可选择 3 位心仪嘉宾。 确认提交后不可再新增选择,但支持修改或撤销已选嘉宾。 管理员可设置匹配截止时间,到达截止时间后系统自动锁定,禁止任何修改。 5. 匹配判定规则 双向互选:若双方恰好互选,则判定为匹配成功,双方均可查看匹配结果。 多向匹配:用户同时与多位嘉宾匹配成功,全部同时展示。 单向选择:A选择B但B未选择A,A可在"谁选了我"页面看到B。 6. 用户页面说明 异性嘉宾列表:展示所有异性嘉宾资料,支持选择/取消选择。 我的选择:展示当前已选的3位嘉宾,支持修改/撤销(截止前)。 谁选了我:展示所有选择了自己的嘉宾(无论自己是否选择了对方)。 匹配成功:展示所有双向互选的匹配对象,支持查看对方完整资料。
2026-05-17 10:23:02 +08:00
@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()}