import uuid from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.ext.asyncio import AsyncSession from app.core.deps import get_db_session, require_study_member, require_study_roles from app.crud import member as member_crud from app.crud import study as study_crud from app.crud import user as user_crud from app.schemas.member import StudyMemberCreate, StudyMemberRead, StudyMemberReadWithUser, StudyMemberUpdate from app.schemas.user import UserDisplay router = APIRouter() async def _ensure_study_exists(db: AsyncSession, study_id: uuid.UUID): study = await study_crud.get(db, study_id) if not study: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Study not found") return study @router.post( "/", response_model=StudyMemberRead, status_code=status.HTTP_201_CREATED, dependencies=[Depends(require_study_roles(["PM"]))], ) async def add_member( study_id: uuid.UUID, member_in: StudyMemberCreate, db: AsyncSession = Depends(get_db_session), ) -> StudyMemberRead: await _ensure_study_exists(db, study_id) existing = await member_crud.get_member(db, study_id, member_in.user_id) if existing: raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Member already exists") member = await member_crud.add_member(db, study_id, member_in) return member @router.get( "/", response_model=list[StudyMemberReadWithUser], dependencies=[Depends(require_study_member())], ) async def list_members( study_id: uuid.UUID, skip: int = 0, limit: int = 100, db: AsyncSession = Depends(get_db_session), ) -> list[StudyMemberReadWithUser]: await _ensure_study_exists(db, study_id) members = await member_crud.list_members(db, study_id, skip=skip, limit=limit) user_ids = {m.user_id for m in members} users_map = await user_crud.get_users_by_ids(db, user_ids) result: list[StudyMemberReadWithUser] = [] for m in members: # 仅返回项目内启用的成员 + 账号启用的用户 if not m.is_active: continue user = users_map.get(m.user_id) if not user or not user.is_active: continue result.append( StudyMemberReadWithUser( id=m.id, study_id=m.study_id, user_id=m.user_id, role_in_study=m.role_in_study, is_active=m.is_active, added_at=m.added_at, user=UserDisplay.model_validate(user) if user else None, ) ) return result @router.patch( "/{member_id}", response_model=StudyMemberRead, dependencies=[Depends(require_study_roles(["PM"]))], ) async def update_member( study_id: uuid.UUID, member_id: uuid.UUID, member_in: StudyMemberUpdate, db: AsyncSession = Depends(get_db_session), ) -> StudyMemberRead: await _ensure_study_exists(db, study_id) member = await member_crud.get_member_by_id(db, member_id) if not member or member.study_id != study_id: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Member not found") updated = await member_crud.update_member(db, member, member_in) return updated @router.delete( "/{member_id}", response_model=StudyMemberRead, dependencies=[Depends(require_study_roles(["PM"]))], ) async def remove_member( study_id: uuid.UUID, member_id: uuid.UUID, db: AsyncSession = Depends(get_db_session), ) -> StudyMemberRead: await _ensure_study_exists(db, study_id) member = await member_crud.get_member_by_id(db, member_id) if not member or member.study_id != study_id: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Member not found") removed = await member_crud.remove_member(db, member) return removed