Initial project commit
This commit is contained in:
@@ -0,0 +1,388 @@
|
||||
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"}
|
||||
Reference in New Issue
Block a user