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"}