"""项目角色生效管理API""" from __future__ import annotations import uuid from typing import Annotated from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.core.deps import get_db_session, is_system_admin, require_study_roles from app.models.study import Study from app.models.study_member import StudyMember router = APIRouter(prefix="/active-roles", tags=["active-roles"]) def _normalize_active_roles(value: object, *, preserve_pm: bool = False) -> list[str]: if not isinstance(value, list): raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="active_roles 必须是数组") roles: list[str] = [] for item in value: role = str(item or "").strip() if not role: continue if role == "ADMIN": raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="ADMIN 不能作为项目角色") if len(role) > 20: raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="项目角色长度不能超过20个字符") if role not in roles: roles.append(role) if preserve_pm and "PM" not in roles: roles.insert(0, "PM") return roles async def _ensure_removed_roles_unused( db: AsyncSession, study_id: uuid.UUID, current_roles: object, next_roles: list[str], ) -> None: if not isinstance(current_roles, list): return removed_roles = [role for role in current_roles if isinstance(role, str) and role not in next_roles] if not removed_roles: return result = await db.execute( select(StudyMember.role_in_study) .where( StudyMember.study_id == study_id, StudyMember.is_active.is_(True), StudyMember.role_in_study.in_(removed_roles), ) .order_by(StudyMember.role_in_study) ) role_in_use = result.scalars().first() if role_in_use: raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail=f"角色 {role_in_use} 仍有成员使用,不能停用", ) @router.get("", summary="获取项目已生效角色列表") async def get_active_roles( study_id: uuid.UUID, _=Depends(require_study_roles(["PM"])), db: Annotated[AsyncSession, Depends(get_db_session)] = None, ) -> dict: result = await db.execute(select(Study).where(Study.id == study_id)) study = result.scalar_one_or_none() if not study: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="项目不存在") return {"active_roles": study.active_roles or []} @router.put("", summary="更新项目已生效角色列表") async def update_active_roles( study_id: uuid.UUID, payload: dict, current_user=Depends(require_study_roles(["PM"])), db: Annotated[AsyncSession, Depends(get_db_session)] = None, ) -> dict: result = await db.execute(select(Study).where(Study.id == study_id)) study = result.scalar_one_or_none() if not study: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="项目不存在") active_roles = _normalize_active_roles( payload.get("active_roles", []), preserve_pm=not is_system_admin(current_user), ) await _ensure_removed_roles_unused(db, study_id, study.active_roles, active_roles) study.active_roles = active_roles await db.commit() return {"active_roles": study.active_roles}