ab4f0d93ed
1、合并列表与详情读取权限,统一使用 subjects:read、project_members:read、monitoring_issues:read 等业务读取 key。 2、增加历史读取权限 key 的兼容映射,确保存量项目权限覆盖值不会丢失。 3、同步前端权限工具、路由权限与权限管理页面,避免继续引用已合并的旧权限 key。 4、补充项目角色启停保护与抽屉未保存变更守卫,防止停用 PM 或仍有关联成员的角色。
99 lines
3.5 KiB
Python
99 lines
3.5 KiB
Python
"""项目角色生效管理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}
|