Files
CosScene/server/app/api/v1/endpoints/events.py
T
2026-05-09 16:40:29 +08:00

389 lines
14 KiB
Python

from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.deps import get_current_active_user, get_db, get_optional_current_user
from app.models.event import Event, EventPhoto, EventRegistration
from app.models.user import User
from app.schemas.common import PageResponse
from app.schemas.event import (
EventBrief,
EventCreate,
EventDetail,
EventPhotoCreate,
EventPhotoOut,
EventUpdate,
RegistrationOut,
)
from app.services.notification_service import send_notification
router = APIRouter()
def _to_brief(ev: Event) -> EventBrief:
return EventBrief(
id=ev.id,
title=ev.title,
city=ev.city,
cover_url=ev.cover_url,
location_name=ev.location_name,
start_time=ev.start_time,
end_time=ev.end_time,
status=ev.status,
audit_status=ev.audit_status,
creator=ev.creator,
registration_count=ev.registration_count,
max_participants=ev.max_participants,
created_at=ev.created_at,
)
# --- Event CRUD ---
@router.post("/", response_model=EventBrief, status_code=status.HTTP_201_CREATED)
async def create_event(
payload: EventCreate,
current_user: User = Depends(get_current_active_user),
db: AsyncSession = Depends(get_db),
):
ev = Event(
creator_id=current_user.id,
title=payload.title,
city=payload.city,
description=payload.description,
cover_url=payload.cover_url,
location_name=payload.location_name,
start_time=payload.start_time,
end_time=payload.end_time,
max_participants=payload.max_participants,
spot_id=payload.spot_id,
status="upcoming",
audit_status="pending",
)
db.add(ev)
await db.commit()
await db.refresh(ev)
return _to_brief(ev)
@router.get("/", response_model=PageResponse[EventBrief])
async def list_events(
city: str | None = None,
status_filter: str | None = Query(None, alias="status"),
page: int = Query(default=1, ge=1),
page_size: int = Query(default=20, ge=1, le=100),
db: AsyncSession = Depends(get_db),
):
base = select(Event).where(Event.audit_status == "approved")
count_base = select(func.count(Event.id)).where(Event.audit_status == "approved")
if city:
base = base.where(Event.city.ilike(f"%{city}%"))
count_base = count_base.where(Event.city.ilike(f"%{city}%"))
if status_filter:
base = base.where(Event.status == status_filter)
count_base = count_base.where(Event.status == status_filter)
total = (await db.execute(count_base)).scalar() or 0
offset = (page - 1) * page_size
result = await db.execute(base.order_by(Event.start_time.asc().nulls_last()).offset(offset).limit(page_size))
items = result.scalars().all()
return PageResponse(total=total, items=[_to_brief(ev) for ev in items])
@router.get("/mine", response_model=PageResponse[EventBrief])
async def list_my_events(
page: int = Query(default=1, ge=1),
page_size: int = Query(default=20, ge=1, le=100),
current_user: User = Depends(get_current_active_user),
db: AsyncSession = Depends(get_db),
):
base = select(Event).where(Event.creator_id == current_user.id)
count_base = select(func.count(Event.id)).where(Event.creator_id == current_user.id)
total = (await db.execute(count_base)).scalar() or 0
offset = (page - 1) * page_size
result = await db.execute(base.order_by(Event.created_at.desc()).offset(offset).limit(page_size))
items = result.scalars().all()
return PageResponse(total=total, items=[_to_brief(ev) for ev in items])
@router.get("/my-registrations", response_model=PageResponse[RegistrationOut])
async def list_my_registrations(
page: int = Query(default=1, ge=1),
page_size: int = Query(default=20, ge=1, le=100),
current_user: User = Depends(get_current_active_user),
db: AsyncSession = Depends(get_db),
):
base = select(EventRegistration).where(EventRegistration.user_id == current_user.id)
count_base = select(func.count(EventRegistration.id)).where(EventRegistration.user_id == current_user.id)
total = (await db.execute(count_base)).scalar() or 0
offset = (page - 1) * page_size
result = await db.execute(base.order_by(EventRegistration.created_at.desc()).offset(offset).limit(page_size))
items = result.scalars().all()
return PageResponse(total=total, items=items)
@router.get("/{event_id}", response_model=EventDetail)
async def get_event(
event_id: int,
current_user: User | None = Depends(get_optional_current_user),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(select(Event).where(Event.id == event_id))
ev = result.scalar_one_or_none()
if not ev:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Not found")
has_registered = False
if current_user:
reg = await db.execute(
select(EventRegistration).where(
EventRegistration.event_id == event_id,
EventRegistration.user_id == current_user.id,
)
)
has_registered = reg.scalar_one_or_none() is not None
photos_result = await db.execute(
select(EventPhoto).where(EventPhoto.event_id == event_id).order_by(EventPhoto.created_at.desc()).limit(50)
)
photos = photos_result.scalars().all()
return EventDetail(
id=ev.id,
title=ev.title,
city=ev.city,
cover_url=ev.cover_url,
location_name=ev.location_name,
start_time=ev.start_time,
end_time=ev.end_time,
status=ev.status,
audit_status=ev.audit_status,
creator=ev.creator,
registration_count=ev.registration_count,
max_participants=ev.max_participants,
created_at=ev.created_at,
description=ev.description,
spot_id=ev.spot_id,
reject_reason=ev.reject_reason,
has_registered=has_registered,
photos=photos,
)
@router.put("/{event_id}", response_model=EventBrief)
async def update_event(
event_id: int,
payload: EventUpdate,
current_user: User = Depends(get_current_active_user),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(select(Event).where(Event.id == event_id))
ev = result.scalar_one_or_none()
if not ev:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Not found")
if ev.creator_id != current_user.id and current_user.role not in ("admin", "moderator"):
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not allowed")
for field, value in payload.model_dump(exclude_unset=True).items():
setattr(ev, field, value)
await db.commit()
await db.refresh(ev)
return _to_brief(ev)
@router.post("/{event_id}/cancel")
async def cancel_event(
event_id: int,
current_user: User = Depends(get_current_active_user),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(select(Event).where(Event.id == event_id))
ev = result.scalar_one_or_none()
if not ev:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Not found")
if ev.creator_id != current_user.id and current_user.role not in ("admin", "moderator"):
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not allowed")
ev.status = "cancelled"
regs = await db.execute(
select(EventRegistration).where(EventRegistration.event_id == event_id)
)
for reg in regs.scalars().all():
await send_notification(
db, reg.user_id, "event",
f"活动「{ev.title}」已取消",
content="很遗憾,该活动已被取消。",
ref_type="event", ref_id=ev.id,
)
await db.commit()
return {"code": 0, "message": "cancelled"}
# --- Registration ---
@router.post("/{event_id}/register", response_model=RegistrationOut, status_code=status.HTTP_201_CREATED)
async def register_event(
event_id: int,
current_user: User = Depends(get_current_active_user),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(select(Event).where(Event.id == event_id))
ev = result.scalar_one_or_none()
if not ev:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Not found")
if ev.audit_status != "approved":
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="活动尚未通过审核")
if ev.status not in ("upcoming",):
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="活动不在报名阶段")
if ev.max_participants > 0 and ev.registration_count >= ev.max_participants:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="报名人数已满")
existing = await db.execute(
select(EventRegistration).where(
EventRegistration.event_id == event_id,
EventRegistration.user_id == current_user.id,
)
)
if existing.scalar_one_or_none():
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="已经报名过了")
reg = EventRegistration(event_id=event_id, user_id=current_user.id, status="registered")
db.add(reg)
ev.registration_count += 1
await send_notification(
db, ev.creator_id, "event",
f"{current_user.nickname} 报名了您的活动「{ev.title}",
ref_type="event", ref_id=ev.id,
)
await db.commit()
await db.refresh(reg)
return reg
@router.delete("/{event_id}/register")
async def cancel_registration(
event_id: int,
current_user: User = Depends(get_current_active_user),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(
select(EventRegistration).where(
EventRegistration.event_id == event_id,
EventRegistration.user_id == current_user.id,
)
)
reg = result.scalar_one_or_none()
if not reg:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="未找到报名记录")
ev_result = await db.execute(select(Event).where(Event.id == event_id))
ev = ev_result.scalar_one_or_none()
if ev and ev.registration_count > 0:
ev.registration_count -= 1
await db.delete(reg)
await db.commit()
return {"code": 0, "message": "取消报名成功"}
@router.get("/{event_id}/registrations", response_model=list[RegistrationOut])
async def list_registrations(
event_id: int,
current_user: User = Depends(get_current_active_user),
db: AsyncSession = Depends(get_db),
):
ev_result = await db.execute(select(Event).where(Event.id == event_id))
ev = ev_result.scalar_one_or_none()
if not ev:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Not found")
if ev.creator_id != current_user.id and current_user.role not in ("admin", "moderator"):
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not allowed")
result = await db.execute(
select(EventRegistration)
.where(EventRegistration.event_id == event_id)
.order_by(EventRegistration.created_at.desc())
)
return result.scalars().all()
# --- Event Photos ---
@router.post("/{event_id}/photos", response_model=EventPhotoOut, status_code=status.HTTP_201_CREATED)
async def add_event_photo(
event_id: int,
payload: EventPhotoCreate,
current_user: User = Depends(get_current_active_user),
db: AsyncSession = Depends(get_db),
):
ev_result = await db.execute(select(Event).where(Event.id == event_id))
ev = ev_result.scalar_one_or_none()
if not ev:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Not found")
reg = await db.execute(
select(EventRegistration).where(
EventRegistration.event_id == event_id,
EventRegistration.user_id == current_user.id,
)
)
is_participant = reg.scalar_one_or_none() is not None
is_creator = ev.creator_id == current_user.id
is_admin = current_user.role in ("admin", "moderator")
if not (is_participant or is_creator or is_admin):
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="只有活动参与者才能上传照片")
photo = EventPhoto(
event_id=event_id,
uploader_id=current_user.id,
image_url=payload.image_url,
caption=payload.caption,
spot_id=payload.spot_id,
)
db.add(photo)
await db.commit()
await db.refresh(photo)
return photo
@router.get("/{event_id}/photos", response_model=list[EventPhotoOut])
async def list_event_photos(
event_id: int,
db: AsyncSession = Depends(get_db),
):
result = await db.execute(
select(EventPhoto)
.where(EventPhoto.event_id == event_id)
.order_by(EventPhoto.created_at.desc())
)
return result.scalars().all()
@router.delete("/{event_id}/photos/{photo_id}")
async def delete_event_photo(
event_id: int,
photo_id: int,
current_user: User = Depends(get_current_active_user),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(
select(EventPhoto).where(EventPhoto.id == photo_id, EventPhoto.event_id == event_id)
)
photo = result.scalar_one_or_none()
if not photo:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Not found")
if photo.uploader_id != current_user.id and current_user.role not in ("admin", "moderator"):
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not allowed")
await db.delete(photo)
await db.commit()
return {"code": 0, "message": "deleted"}