403 lines
15 KiB
Python
403 lines
15 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.shooting import ShootingApplication, ShootingRequest
|
|
from app.models.user import User
|
|
from app.schemas.common import PageResponse
|
|
from app.schemas.shooting import (
|
|
ApplicationCreate,
|
|
ApplicationOut,
|
|
ShootingRequestBrief,
|
|
ShootingRequestCreate,
|
|
ShootingRequestDetail,
|
|
ShootingRequestUpdate,
|
|
)
|
|
from app.services.notification_service import send_notification
|
|
|
|
router = APIRouter()
|
|
|
|
ROLE_LABELS = {
|
|
"photographer": "摄影师",
|
|
"cosplayer": "Coser",
|
|
"both": "不限",
|
|
}
|
|
|
|
|
|
def _to_brief(sr: ShootingRequest) -> ShootingRequestBrief:
|
|
return ShootingRequestBrief(
|
|
id=sr.id,
|
|
title=sr.title,
|
|
city=sr.city,
|
|
style=sr.style,
|
|
shoot_date=sr.shoot_date,
|
|
is_free=sr.is_free,
|
|
budget_min=float(sr.budget_min) if sr.budget_min is not None else None,
|
|
budget_max=float(sr.budget_max) if sr.budget_max is not None else None,
|
|
role_needed=sr.role_needed,
|
|
status=sr.status,
|
|
audit_status=sr.audit_status,
|
|
creator=sr.creator,
|
|
application_count=len(sr.applications) if sr.applications else 0,
|
|
created_at=sr.created_at,
|
|
)
|
|
|
|
|
|
# --- ShootingRequest CRUD ---
|
|
|
|
@router.post("/", response_model=ShootingRequestBrief, status_code=status.HTTP_201_CREATED)
|
|
async def create_shooting_request(
|
|
payload: ShootingRequestCreate,
|
|
current_user: User = Depends(get_current_active_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
sr = ShootingRequest(
|
|
creator_id=current_user.id,
|
|
title=payload.title,
|
|
city=payload.city,
|
|
description=payload.description,
|
|
style=payload.style,
|
|
shoot_date=payload.shoot_date,
|
|
is_free=payload.is_free,
|
|
budget_min=payload.budget_min if not payload.is_free else None,
|
|
budget_max=payload.budget_max if not payload.is_free else None,
|
|
role_needed=payload.role_needed,
|
|
max_applicants=payload.max_applicants,
|
|
contact_info=payload.contact_info,
|
|
spot_id=payload.spot_id,
|
|
status="open",
|
|
audit_status="pending",
|
|
)
|
|
db.add(sr)
|
|
await db.commit()
|
|
await db.refresh(sr)
|
|
return _to_brief(sr)
|
|
|
|
|
|
@router.get("/", response_model=PageResponse[ShootingRequestBrief])
|
|
async def list_shooting_requests(
|
|
city: str | None = None,
|
|
style: str | None = None,
|
|
role_needed: str | None = None,
|
|
is_free: bool | 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(ShootingRequest).where(ShootingRequest.audit_status == "approved")
|
|
count_base = select(func.count(ShootingRequest.id)).where(ShootingRequest.audit_status == "approved")
|
|
|
|
if city:
|
|
base = base.where(ShootingRequest.city.ilike(f"%{city}%"))
|
|
count_base = count_base.where(ShootingRequest.city.ilike(f"%{city}%"))
|
|
if style:
|
|
base = base.where(ShootingRequest.style.ilike(f"%{style}%"))
|
|
count_base = count_base.where(ShootingRequest.style.ilike(f"%{style}%"))
|
|
if role_needed:
|
|
base = base.where(ShootingRequest.role_needed == role_needed)
|
|
count_base = count_base.where(ShootingRequest.role_needed == role_needed)
|
|
if is_free is not None:
|
|
base = base.where(ShootingRequest.is_free == is_free)
|
|
count_base = count_base.where(ShootingRequest.is_free == is_free)
|
|
if status_filter:
|
|
base = base.where(ShootingRequest.status == status_filter)
|
|
count_base = count_base.where(ShootingRequest.status == status_filter)
|
|
|
|
total = (await db.execute(count_base)).scalar() or 0
|
|
offset = (page - 1) * page_size
|
|
result = await db.execute(base.order_by(ShootingRequest.created_at.desc()).offset(offset).limit(page_size))
|
|
items = result.scalars().all()
|
|
|
|
return PageResponse(total=total, items=[_to_brief(sr) for sr in items])
|
|
|
|
|
|
@router.get("/mine", response_model=PageResponse[ShootingRequestBrief])
|
|
async def list_my_shooting_requests(
|
|
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(ShootingRequest).where(ShootingRequest.creator_id == current_user.id)
|
|
count_base = select(func.count(ShootingRequest.id)).where(ShootingRequest.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(ShootingRequest.created_at.desc()).offset(offset).limit(page_size))
|
|
items = result.scalars().all()
|
|
|
|
return PageResponse(total=total, items=[_to_brief(sr) for sr in items])
|
|
|
|
|
|
@router.get("/my-applications", response_model=PageResponse[ApplicationOut])
|
|
async def list_my_applications(
|
|
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(ShootingApplication).where(ShootingApplication.applicant_id == current_user.id)
|
|
count_base = select(func.count(ShootingApplication.id)).where(ShootingApplication.applicant_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(ShootingApplication.created_at.desc()).offset(offset).limit(page_size))
|
|
items = result.scalars().all()
|
|
|
|
return PageResponse(total=total, items=items)
|
|
|
|
|
|
@router.get("/{request_id}", response_model=ShootingRequestDetail)
|
|
async def get_shooting_request(
|
|
request_id: int,
|
|
current_user: User | None = Depends(get_optional_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
result = await db.execute(select(ShootingRequest).where(ShootingRequest.id == request_id))
|
|
sr = result.scalar_one_or_none()
|
|
if not sr:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Not found")
|
|
|
|
has_applied = False
|
|
if current_user:
|
|
app_result = await db.execute(
|
|
select(ShootingApplication).where(
|
|
ShootingApplication.request_id == request_id,
|
|
ShootingApplication.applicant_id == current_user.id,
|
|
)
|
|
)
|
|
has_applied = app_result.scalar_one_or_none() is not None
|
|
|
|
return ShootingRequestDetail(
|
|
id=sr.id,
|
|
title=sr.title,
|
|
city=sr.city,
|
|
style=sr.style,
|
|
shoot_date=sr.shoot_date,
|
|
is_free=sr.is_free,
|
|
budget_min=float(sr.budget_min) if sr.budget_min is not None else None,
|
|
budget_max=float(sr.budget_max) if sr.budget_max is not None else None,
|
|
role_needed=sr.role_needed,
|
|
status=sr.status,
|
|
audit_status=sr.audit_status,
|
|
creator=sr.creator,
|
|
application_count=len(sr.applications) if sr.applications else 0,
|
|
created_at=sr.created_at,
|
|
description=sr.description,
|
|
max_applicants=sr.max_applicants,
|
|
contact_info=sr.contact_info if (current_user and (sr.creator_id == current_user.id or current_user.role in ("admin", "moderator"))) else None,
|
|
spot_id=sr.spot_id,
|
|
reject_reason=sr.reject_reason,
|
|
has_applied=has_applied,
|
|
)
|
|
|
|
|
|
@router.put("/{request_id}", response_model=ShootingRequestBrief)
|
|
async def update_shooting_request(
|
|
request_id: int,
|
|
payload: ShootingRequestUpdate,
|
|
current_user: User = Depends(get_current_active_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
result = await db.execute(select(ShootingRequest).where(ShootingRequest.id == request_id))
|
|
sr = result.scalar_one_or_none()
|
|
if not sr:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Not found")
|
|
if sr.creator_id != current_user.id and current_user.role not in ("admin", "moderator"):
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not allowed")
|
|
|
|
update_data = payload.model_dump(exclude_unset=True)
|
|
for field, value in update_data.items():
|
|
setattr(sr, field, value)
|
|
await db.commit()
|
|
await db.refresh(sr)
|
|
return _to_brief(sr)
|
|
|
|
|
|
@router.post("/{request_id}/close")
|
|
async def close_shooting_request(
|
|
request_id: int,
|
|
current_user: User = Depends(get_current_active_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
result = await db.execute(select(ShootingRequest).where(ShootingRequest.id == request_id))
|
|
sr = result.scalar_one_or_none()
|
|
if not sr:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Not found")
|
|
if sr.creator_id != current_user.id and current_user.role not in ("admin", "moderator"):
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not allowed")
|
|
|
|
sr.status = "closed"
|
|
await db.commit()
|
|
return {"code": 0, "message": "closed"}
|
|
|
|
|
|
# --- Applications ---
|
|
|
|
@router.post("/{request_id}/apply", response_model=ApplicationOut, status_code=status.HTTP_201_CREATED)
|
|
async def apply_to_shooting(
|
|
request_id: int,
|
|
payload: ApplicationCreate,
|
|
current_user: User = Depends(get_current_active_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
result = await db.execute(select(ShootingRequest).where(ShootingRequest.id == request_id))
|
|
sr = result.scalar_one_or_none()
|
|
if not sr:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Not found")
|
|
if sr.status != "open":
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="该约拍已关闭")
|
|
if sr.audit_status != "approved":
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="该约拍尚未通过审核")
|
|
if sr.creator_id == current_user.id:
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="不能报名自己的约拍")
|
|
|
|
existing = await db.execute(
|
|
select(ShootingApplication).where(
|
|
ShootingApplication.request_id == request_id,
|
|
ShootingApplication.applicant_id == current_user.id,
|
|
)
|
|
)
|
|
if existing.scalar_one_or_none():
|
|
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="已经报名过了")
|
|
|
|
app = ShootingApplication(
|
|
request_id=request_id,
|
|
applicant_id=current_user.id,
|
|
message=payload.message,
|
|
status="pending",
|
|
)
|
|
db.add(app)
|
|
|
|
await send_notification(
|
|
db, sr.creator_id, "shooting",
|
|
f"有人报名了您的约拍「{sr.title}」",
|
|
content=f"{current_user.nickname} 报名了您的约拍",
|
|
ref_type="shooting", ref_id=sr.id,
|
|
)
|
|
|
|
await db.commit()
|
|
await db.refresh(app)
|
|
return app
|
|
|
|
|
|
@router.get("/{request_id}/applications", response_model=list[ApplicationOut])
|
|
async def list_applications(
|
|
request_id: int,
|
|
current_user: User = Depends(get_current_active_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
result = await db.execute(select(ShootingRequest).where(ShootingRequest.id == request_id))
|
|
sr = result.scalar_one_or_none()
|
|
if not sr:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Not found")
|
|
if sr.creator_id != current_user.id and current_user.role not in ("admin", "moderator"):
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Only creator can view applications")
|
|
|
|
result = await db.execute(
|
|
select(ShootingApplication)
|
|
.where(ShootingApplication.request_id == request_id)
|
|
.order_by(ShootingApplication.created_at.desc())
|
|
)
|
|
return result.scalars().all()
|
|
|
|
|
|
@router.post("/{request_id}/applications/{app_id}/accept")
|
|
async def accept_application(
|
|
request_id: int,
|
|
app_id: int,
|
|
current_user: User = Depends(get_current_active_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
sr_result = await db.execute(select(ShootingRequest).where(ShootingRequest.id == request_id))
|
|
sr = sr_result.scalar_one_or_none()
|
|
if not sr or sr.creator_id != current_user.id:
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not allowed")
|
|
|
|
app_result = await db.execute(
|
|
select(ShootingApplication).where(
|
|
ShootingApplication.id == app_id,
|
|
ShootingApplication.request_id == request_id,
|
|
)
|
|
)
|
|
app = app_result.scalar_one_or_none()
|
|
if not app:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Application not found")
|
|
if app.status != "pending":
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="该报名已被处理")
|
|
|
|
app.status = "accepted"
|
|
|
|
accepted_count = sum(1 for a in sr.applications if a.status == "accepted") + 1
|
|
if accepted_count >= sr.max_applicants:
|
|
sr.status = "matched"
|
|
|
|
await send_notification(
|
|
db, app.applicant_id, "shooting",
|
|
f"您的约拍报名「{sr.title}」已被接受",
|
|
content="对方已接受您的报名,请查看详情。",
|
|
ref_type="shooting", ref_id=sr.id,
|
|
)
|
|
|
|
await db.commit()
|
|
return {"code": 0, "message": "accepted"}
|
|
|
|
|
|
@router.post("/{request_id}/applications/{app_id}/reject")
|
|
async def reject_application(
|
|
request_id: int,
|
|
app_id: int,
|
|
current_user: User = Depends(get_current_active_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
sr_result = await db.execute(select(ShootingRequest).where(ShootingRequest.id == request_id))
|
|
sr = sr_result.scalar_one_or_none()
|
|
if not sr or sr.creator_id != current_user.id:
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not allowed")
|
|
|
|
app_result = await db.execute(
|
|
select(ShootingApplication).where(
|
|
ShootingApplication.id == app_id,
|
|
ShootingApplication.request_id == request_id,
|
|
)
|
|
)
|
|
app = app_result.scalar_one_or_none()
|
|
if not app:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Application not found")
|
|
|
|
app.status = "rejected"
|
|
|
|
await send_notification(
|
|
db, app.applicant_id, "shooting",
|
|
f"您的约拍报名「{sr.title}」未通过",
|
|
ref_type="shooting", ref_id=sr.id,
|
|
)
|
|
|
|
await db.commit()
|
|
return {"code": 0, "message": "rejected"}
|
|
|
|
|
|
@router.delete("/{request_id}/applications/withdraw")
|
|
async def withdraw_application(
|
|
request_id: int,
|
|
current_user: User = Depends(get_current_active_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
result = await db.execute(
|
|
select(ShootingApplication).where(
|
|
ShootingApplication.request_id == request_id,
|
|
ShootingApplication.applicant_id == current_user.id,
|
|
)
|
|
)
|
|
app = result.scalar_one_or_none()
|
|
if not app:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Application not found")
|
|
if app.status == "accepted":
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="已被接受的报名无法撤回")
|
|
|
|
await db.delete(app)
|
|
await db.commit()
|
|
return {"code": 0, "message": "withdrawn"}
|