389 lines
14 KiB
Python
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"}
|