统一项目权限读取模型
1、合并列表与详情读取权限,统一使用 subjects:read、project_members:read、monitoring_issues:read 等业务读取 key。 2、增加历史读取权限 key 的兼容映射,确保存量项目权限覆盖值不会丢失。 3、同步前端权限工具、路由权限与权限管理页面,避免继续引用已合并的旧权限 key。 4、补充项目角色启停保护与抽屉未保存变更守卫,防止停用 PM 或仍有关联成员的角色。
This commit is contained in:
@@ -122,7 +122,7 @@ async def _list_ae_records(
|
||||
@router.get(
|
||||
"/summary",
|
||||
response_model=list[AERead],
|
||||
dependencies=[Depends(require_api_permission("subject_aes:list"))],
|
||||
dependencies=[Depends(require_api_permission("subject_aes:read"))],
|
||||
)
|
||||
async def list_risk_issue_ae(
|
||||
study_id: uuid.UUID,
|
||||
@@ -149,7 +149,7 @@ async def list_risk_issue_ae(
|
||||
@router.get(
|
||||
"/",
|
||||
response_model=list[AERead],
|
||||
dependencies=[Depends(require_api_permission("subject_aes:list"))],
|
||||
dependencies=[Depends(require_api_permission("subject_aes:read"))],
|
||||
)
|
||||
async def list_ae(
|
||||
study_id: uuid.UUID,
|
||||
|
||||
@@ -142,10 +142,10 @@ async def add_member(
|
||||
@router.get(
|
||||
"/",
|
||||
response_model=list[StudyMemberReadWithUser],
|
||||
dependencies=[Depends(require_api_permission("project_members:list"))],
|
||||
dependencies=[Depends(require_api_permission("project_members:read"))],
|
||||
)
|
||||
@register_api_endpoint(
|
||||
endpoint_key="project_members:list",
|
||||
endpoint_key="project_members:read",
|
||||
module="project_members",
|
||||
action="read",
|
||||
description="查询项目成员列表",
|
||||
@@ -188,10 +188,10 @@ async def list_members(
|
||||
@router.get(
|
||||
"/candidates",
|
||||
response_model=list[UserResponse],
|
||||
dependencies=[Depends(require_api_permission("project_members:candidates"))],
|
||||
dependencies=[Depends(require_api_permission("project_members:read"))],
|
||||
)
|
||||
@register_api_endpoint(
|
||||
endpoint_key="project_members:candidates",
|
||||
endpoint_key="project_members:read",
|
||||
module="project_members",
|
||||
action="read",
|
||||
description="查询项目成员候选人",
|
||||
|
||||
@@ -332,7 +332,7 @@ def _normalize_file_rows(filename: str, content: bytes) -> list[dict[str, object
|
||||
@router.get(
|
||||
"/issues",
|
||||
response_model=list[MonitoringVisitIssueRead],
|
||||
dependencies=[Depends(require_api_permission("monitoring_issues:list"))],
|
||||
dependencies=[Depends(require_api_permission("monitoring_issues:read"))],
|
||||
)
|
||||
async def list_monitoring_visit_issues(
|
||||
study_id: uuid.UUID,
|
||||
@@ -421,7 +421,7 @@ async def create_monitoring_visit_issue(
|
||||
@router.get(
|
||||
"/issues/export",
|
||||
response_class=StreamingResponse,
|
||||
dependencies=[Depends(require_api_permission("monitoring_issues:list"))],
|
||||
dependencies=[Depends(require_api_permission("monitoring_issues:read"))],
|
||||
)
|
||||
async def export_monitoring_visit_issues(
|
||||
study_id: uuid.UUID,
|
||||
|
||||
@@ -9,13 +9,14 @@ 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, require_study_roles
|
||||
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) -> list[str]:
|
||||
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] = []
|
||||
@@ -25,15 +26,44 @@ def _normalize_active_roles(value: object) -> list[str]:
|
||||
continue
|
||||
if role == "ADMIN":
|
||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="ADMIN 不能作为项目角色")
|
||||
if role == "QA":
|
||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="QA 不能作为项目角色")
|
||||
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,
|
||||
@@ -51,13 +81,18 @@ async def get_active_roles(
|
||||
async def update_active_roles(
|
||||
study_id: uuid.UUID,
|
||||
payload: dict,
|
||||
_=Depends(require_study_roles(["PM"])),
|
||||
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="项目不存在")
|
||||
study.active_roles = _normalize_active_roles(payload.get("active_roles", []))
|
||||
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}
|
||||
|
||||
@@ -66,7 +66,7 @@ async def create_history(
|
||||
@router.get(
|
||||
"/histories",
|
||||
response_model=list[SubjectHistoryRead],
|
||||
dependencies=[Depends(require_api_permission("subject_histories:list"))],
|
||||
dependencies=[Depends(require_api_permission("subject_histories:read"))],
|
||||
)
|
||||
async def list_histories(
|
||||
study_id: uuid.UUID,
|
||||
|
||||
@@ -66,7 +66,7 @@ async def create_subject(
|
||||
@router.get(
|
||||
"/",
|
||||
response_model=list[SubjectRead],
|
||||
dependencies=[Depends(require_api_permission("subjects:list"))],
|
||||
dependencies=[Depends(require_api_permission("subjects:read"))],
|
||||
)
|
||||
async def list_subjects(
|
||||
study_id: uuid.UUID,
|
||||
|
||||
Reference in New Issue
Block a user