完全移除模块级权限系统,迁移至接口级权限
后端: - 删除 StudyRolePermission 模型和 project_permissions API 文件 - 重写 project_permissions.py core,移除所有模块级权限函数 - 重写 permission_cache.py,移除模块级权限缓存逻辑 - 新增权限模板功能(PermissionTemplate 模型、API、服务层) - 新增 permission_templates 数据库迁移 - 迁移 8 个模块(attachments、audit_logs、dashboard、faqs、 faq_categories、fees_attachments、knowledge_notes、 material_equipments、overview、subject_histories、subject_pds) 至接口级权限检查 - 删除所有模块级权限相关测试文件,新增权限模板测试 前端: - 删除 ProjectPermissions.vue 和 ProjectPermissionsModule.vue - 重写 projectRoutePermissions.ts,改为基于接口级权限格式 - 更新 store/study.ts、router/index.ts、AuditLogs.vue、Projects.vue 中的权限 API 调用,从 fetchProjectRolePermissions 改为 fetchApiEndpointPermissions - 清理 types/api.ts 中的旧模块级权限类型定义 - 新增 PermissionTemplateSelector.vue 组件 - 更新权限管理页面,移除模块级权限 tab Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+54
-53
@@ -3,8 +3,8 @@ import uuid
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.deps import get_current_user, get_db_session, require_study_not_locked
|
||||
from app.core.project_permissions import role_has_project_permission
|
||||
from app.core.deps import get_current_user, get_db_session, require_study_not_locked, require_api_permission
|
||||
from app.core.project_permissions import role_has_api_permission
|
||||
from app.crud import audit as audit_crud
|
||||
from app.crud import faq_category as category_crud
|
||||
from app.crud import faq_item as faq_crud
|
||||
@@ -31,32 +31,16 @@ def _is_system_admin(current_user) -> bool:
|
||||
return role == "ADMIN"
|
||||
|
||||
|
||||
async def _require_project_permission(
|
||||
db: AsyncSession,
|
||||
study_id: uuid.UUID | None,
|
||||
current_user,
|
||||
action: str,
|
||||
):
|
||||
if not study_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="FAQ 不存在")
|
||||
if _is_system_admin(current_user):
|
||||
return None
|
||||
member = await member_crud.get_member(db, study_id, current_user.id)
|
||||
if not member or not member.is_active:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="不是该项目成员")
|
||||
allowed = await role_has_project_permission(db, study_id, member.role_in_study, "faq", action)
|
||||
if not allowed:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足")
|
||||
return member
|
||||
|
||||
|
||||
@router.post(
|
||||
"/",
|
||||
response_model=FaqRead,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
summary="创建 FAQ",
|
||||
description="创建项目内 FAQ,权限由项目级权限矩阵控制。",
|
||||
dependencies=[Depends(require_study_not_locked())],
|
||||
dependencies=[
|
||||
Depends(require_api_permission("faq:create")),
|
||||
Depends(require_study_not_locked())
|
||||
],
|
||||
)
|
||||
async def create_faq(
|
||||
payload: FaqCreate,
|
||||
@@ -70,7 +54,6 @@ async def create_faq(
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="分类不存在")
|
||||
if cat.study_id != payload.study_id:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="分类范围不匹配")
|
||||
await _require_project_permission(db, payload.study_id, current_user, "write")
|
||||
try:
|
||||
item = await faq_crud.create_item(db, payload, created_by=current_user.id)
|
||||
except ValueError as exc:
|
||||
@@ -102,6 +85,7 @@ async def create_faq(
|
||||
response_model=PaginatedResponse[FaqRead],
|
||||
summary="FAQ 列表",
|
||||
description="返回全局与项目 FAQ,可按关键词、分类过滤。",
|
||||
dependencies=[Depends(require_api_permission("faq:read"))],
|
||||
)
|
||||
async def list_faqs(
|
||||
study_id: uuid.UUID | None = None,
|
||||
@@ -116,14 +100,23 @@ async def list_faqs(
|
||||
async def _get_member_role(sid: uuid.UUID) -> str | None:
|
||||
if sid in membership_cache:
|
||||
return membership_cache[sid]
|
||||
member = await _require_project_permission(db, sid, current_user, "read")
|
||||
role = member.role_in_study if member else "ADMIN"
|
||||
member = await member_crud.get_member(db, sid, current_user.id)
|
||||
role = member.role_in_study if member else None
|
||||
membership_cache[sid] = role
|
||||
return role
|
||||
|
||||
if not study_id:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="必须提供项目 ID")
|
||||
await _require_project_permission(db, study_id, current_user, "write" if is_active is False else "read")
|
||||
|
||||
role_value = current_user.role.value if hasattr(current_user.role, "value") else current_user.role
|
||||
if role_value != "ADMIN":
|
||||
member = await member_crud.get_member(db, study_id, current_user.id)
|
||||
if not member or not member.is_active:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="不是项目成员")
|
||||
if is_active is False:
|
||||
allowed = await role_has_api_permission(db, study_id, member.role_in_study, "faq:update", check_prerequisites=False)
|
||||
if not allowed:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足")
|
||||
|
||||
items = await faq_crud.list_items(
|
||||
db,
|
||||
@@ -141,7 +134,7 @@ async def list_faqs(
|
||||
if not role:
|
||||
continue
|
||||
if not it.is_active and not _is_system_admin(current_user):
|
||||
allowed = await role_has_project_permission(db, it.study_id, role, "faq", "write")
|
||||
allowed = await role_has_api_permission(db, it.study_id, role, "faq:update", check_prerequisites=False)
|
||||
if not allowed:
|
||||
continue
|
||||
visible.append(FaqRead.model_validate(it))
|
||||
@@ -153,6 +146,7 @@ async def list_faqs(
|
||||
response_model=FaqRead,
|
||||
summary="FAQ 详情",
|
||||
description="获取单条 FAQ,停用 FAQ 需项目级写权限。",
|
||||
dependencies=[Depends(require_api_permission("faq:read"))],
|
||||
)
|
||||
async def get_faq(
|
||||
item_id: uuid.UUID,
|
||||
@@ -164,7 +158,14 @@ async def get_faq(
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="FAQ 不存在")
|
||||
if not item.study_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="FAQ 不存在")
|
||||
await _require_project_permission(db, item.study_id, current_user, "write" if not item.is_active else "read")
|
||||
role_value = current_user.role.value if hasattr(current_user.role, "value") else current_user.role
|
||||
if not item.is_active and role_value != "ADMIN":
|
||||
member = await member_crud.get_member(db, item.study_id, current_user.id)
|
||||
if not member or not member.is_active:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="不是项目成员")
|
||||
allowed = await role_has_api_permission(db, item.study_id, member.role_in_study, "faq:update", check_prerequisites=False)
|
||||
if not allowed:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足")
|
||||
return FaqRead.model_validate(item)
|
||||
|
||||
|
||||
@@ -173,7 +174,10 @@ async def get_faq(
|
||||
response_model=FaqRead,
|
||||
summary="更新 FAQ",
|
||||
description="更新 FAQ 内容或启停状态,权限由项目级权限矩阵控制。",
|
||||
dependencies=[Depends(require_study_not_locked())],
|
||||
dependencies=[
|
||||
Depends(require_api_permission("faq:update")),
|
||||
Depends(require_study_not_locked())
|
||||
],
|
||||
)
|
||||
async def update_faq(
|
||||
item_id: uuid.UUID,
|
||||
@@ -184,10 +188,6 @@ async def update_faq(
|
||||
item = await faq_crud.get_item(db, item_id)
|
||||
if not item:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="FAQ 不存在")
|
||||
if item.created_by != current_user.id:
|
||||
await _require_project_permission(db, item.study_id, current_user, "write")
|
||||
else:
|
||||
await _require_project_permission(db, item.study_id, current_user, "write")
|
||||
old_active = item.is_active
|
||||
updated = await faq_crud.update_item(db, item, payload)
|
||||
action = "UPDATE_FAQ_ITEM"
|
||||
@@ -213,7 +213,10 @@ async def update_faq(
|
||||
response_model=FaqRead,
|
||||
summary="更新 FAQ 状态",
|
||||
description="提问者或具备项目级写权限的成员可确认已解决。",
|
||||
dependencies=[Depends(require_study_not_locked())],
|
||||
dependencies=[
|
||||
Depends(require_api_permission("faq:update")),
|
||||
Depends(require_study_not_locked())
|
||||
],
|
||||
)
|
||||
async def update_faq_status(
|
||||
item_id: uuid.UUID,
|
||||
@@ -226,10 +229,6 @@ async def update_faq_status(
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="FAQ 不存在")
|
||||
if payload.status != "RESOLVED":
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="仅允许设置为已解决")
|
||||
if item.created_by != current_user.id:
|
||||
await _require_project_permission(db, item.study_id, current_user, "write")
|
||||
else:
|
||||
await _require_project_permission(db, item.study_id, current_user, "read")
|
||||
await faq_crud.set_status(db, item.id, "RESOLVED", resolved_by_confirm=True)
|
||||
updated = await faq_crud.get_item(db, item_id)
|
||||
return FaqRead.model_validate(updated)
|
||||
@@ -240,7 +239,10 @@ async def update_faq_status(
|
||||
response_model=FaqRead,
|
||||
summary="设置最佳回复",
|
||||
description="项目成员可设置最佳回复,全局仅 ADMIN。",
|
||||
dependencies=[Depends(require_study_not_locked())],
|
||||
dependencies=[
|
||||
Depends(require_api_permission("faq:update")),
|
||||
Depends(require_study_not_locked())
|
||||
],
|
||||
)
|
||||
async def set_best_reply(
|
||||
item_id: uuid.UUID,
|
||||
@@ -253,7 +255,6 @@ async def set_best_reply(
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="FAQ 不存在")
|
||||
if not item.study_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="FAQ 不存在")
|
||||
await _require_project_permission(db, item.study_id, current_user, "write")
|
||||
if payload.best_reply_id:
|
||||
reply = await reply_crud.get_reply(db, payload.best_reply_id)
|
||||
if not reply or reply.faq_id != item.id:
|
||||
@@ -281,6 +282,7 @@ async def set_best_reply(
|
||||
response_model=PaginatedResponse[FaqReplyRead],
|
||||
summary="FAQ 回复列表",
|
||||
description="获取 FAQ 的回复列表。",
|
||||
dependencies=[Depends(require_api_permission("faq:read"))],
|
||||
)
|
||||
async def list_replies(
|
||||
item_id: uuid.UUID,
|
||||
@@ -290,7 +292,6 @@ async def list_replies(
|
||||
item = await faq_crud.get_item(db, item_id)
|
||||
if not item:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="FAQ 不存在")
|
||||
await _require_project_permission(db, item.study_id, current_user, "read")
|
||||
replies = await reply_crud.list_replies(db, item_id)
|
||||
reply_map = {r.id: r for r in replies}
|
||||
result: list[FaqReplyRead] = []
|
||||
@@ -317,7 +318,10 @@ async def list_replies(
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
summary="创建 FAQ 回复",
|
||||
description="回复 FAQ,项目内成员可回复,全局仅 ADMIN。",
|
||||
dependencies=[Depends(require_study_not_locked())],
|
||||
dependencies=[
|
||||
Depends(require_api_permission("faq_reply:create")),
|
||||
Depends(require_study_not_locked())
|
||||
],
|
||||
)
|
||||
async def create_reply(
|
||||
item_id: uuid.UUID,
|
||||
@@ -332,7 +336,6 @@ async def create_reply(
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="FAQ 不存在")
|
||||
if not payload.content.strip():
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="回复内容不能为空")
|
||||
await _require_project_permission(db, item.study_id, current_user, "write")
|
||||
quote = None
|
||||
if payload.quote_reply_id:
|
||||
quote = await reply_crud.get_reply(db, payload.quote_reply_id)
|
||||
@@ -377,7 +380,10 @@ async def create_reply(
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
summary="删除 FAQ",
|
||||
description="删除 FAQ,权限由项目级权限矩阵控制。",
|
||||
dependencies=[Depends(require_study_not_locked())],
|
||||
dependencies=[
|
||||
Depends(require_api_permission("faq:delete")),
|
||||
Depends(require_study_not_locked())
|
||||
],
|
||||
)
|
||||
async def delete_faq(
|
||||
item_id: uuid.UUID,
|
||||
@@ -387,10 +393,6 @@ async def delete_faq(
|
||||
item = await faq_crud.get_item(db, item_id)
|
||||
if not item:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="FAQ 不存在")
|
||||
if item.created_by != current_user.id:
|
||||
await _require_project_permission(db, item.study_id, current_user, "write")
|
||||
else:
|
||||
await _require_project_permission(db, item.study_id, current_user, "write")
|
||||
await reply_crud.delete_replies_by_faq_id(db, item.id)
|
||||
await db.delete(item)
|
||||
await db.commit()
|
||||
@@ -411,7 +413,10 @@ async def delete_faq(
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
summary="删除 FAQ 回复",
|
||||
description="删除 FAQ 回复,回复者或具备项目级写权限的成员可删除。",
|
||||
dependencies=[Depends(require_study_not_locked())],
|
||||
dependencies=[
|
||||
Depends(require_api_permission("faq_reply:delete")),
|
||||
Depends(require_study_not_locked())
|
||||
],
|
||||
)
|
||||
async def delete_reply(
|
||||
item_id: uuid.UUID,
|
||||
@@ -425,10 +430,6 @@ async def delete_reply(
|
||||
reply = await reply_crud.get_reply(db, reply_id)
|
||||
if not reply or reply.faq_id != item.id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="回复不存在")
|
||||
if reply.created_by != current_user.id:
|
||||
await _require_project_permission(db, item.study_id, current_user, "write")
|
||||
else:
|
||||
await _require_project_permission(db, item.study_id, current_user, "read")
|
||||
if item.best_reply_id == reply.id:
|
||||
await faq_crud.set_best_reply(db, item.id, None)
|
||||
ref_count = await reply_crud.count_quote_references(db, reply.id)
|
||||
|
||||
Reference in New Issue
Block a user