feat: add backend rule group and permission support

This commit is contained in:
wren
2026-05-06 09:40:37 +08:00
parent 7acbe0f1d9
commit 76ba7e65ed
45 changed files with 6175 additions and 110 deletions
@@ -12,12 +12,14 @@ from fastapi_common.fastapi_common_web.domain.responses import StatusCodeEnum
from fastapi_common.fastapi_common_web.exception.LeauditException import LeauditException
from fastapi_modules.fastapi_leaudit.domian.Dto.rbacAdminDto import (
RoleAccessSaveDTO,
RoleCreateDTO,
RolePermissionsBatchDTO,
RoleRoutesUpdateDTO,
RoleUpdateDTO,
)
from fastapi_modules.fastapi_leaudit.domian.vo.rbacAdminVo import (
RoleAccessSaveVO,
RoleListVO,
RolePermissionsVO,
RoleRoutesVO,
@@ -156,6 +158,11 @@ class RbacAdminServiceImpl(IRbacAdminService):
{"permission_key": "doc_type:create:write", "display_name": "创建文档类型", "module": "doc_type", "resource": "create", "action": "write", "api_method": "POST", "api_path": "/api/document-types", "route_path": "/document-types"},
{"permission_key": "doc_type:update:write", "display_name": "更新文档类型", "module": "doc_type", "resource": "update", "action": "write", "api_method": "PUT", "api_path": "/api/document-types/{id}", "route_path": "/document-types"},
{"permission_key": "doc_type:delete:delete", "display_name": "删除文档类型", "module": "doc_type", "resource": "delete", "action": "delete", "api_method": "DELETE", "api_path": "/api/document-types/{id}", "route_path": "/document-types"},
{"permission_key": "evaluation_point:list:read", "display_name": "评查点列表", "module": "evaluation_point", "resource": "list", "action": "read", "api_method": "GET", "api_path": "/api/v3/evaluation-points", "route_path": "/rules"},
{"permission_key": "evaluation_point:detail:read", "display_name": "评查点详情", "module": "evaluation_point", "resource": "detail", "action": "read", "api_method": "GET", "api_path": "/api/v3/evaluation-points/{id}", "route_path": "/rules"},
{"permission_key": "evaluation_point:create:write", "display_name": "创建评查点", "module": "evaluation_point", "resource": "create", "action": "write", "api_method": "POST", "api_path": "/api/v3/evaluation-points", "route_path": "/rules"},
{"permission_key": "evaluation_point:update:write", "display_name": "更新评查点", "module": "evaluation_point", "resource": "update", "action": "write", "api_method": "PUT", "api_path": "/api/v3/evaluation-points/{id}", "route_path": "/rules"},
{"permission_key": "evaluation_point:delete:delete", "display_name": "删除评查点", "module": "evaluation_point", "resource": "delete", "action": "delete", "api_method": "DELETE", "api_path": "/api/v3/evaluation-points/{id}", "route_path": "/rules"},
{"permission_key": "rbac:roles:read", "display_name": "角色列表", "module": "rbac", "resource": "roles", "action": "read", "api_method": "GET", "api_path": "/api/v3/rbac/roles", "route_path": "/role-permissions"},
{"permission_key": "rbac:roles:create", "display_name": "创建角色", "module": "rbac", "resource": "roles", "action": "create", "api_method": "POST", "api_path": "/api/v3/rbac/roles", "route_path": "/role-permissions"},
{"permission_key": "rbac:roles:update", "display_name": "更新角色", "module": "rbac", "resource": "roles", "action": "update", "api_method": "PUT", "api_path": "/api/v3/rbac/roles/{role_id}", "route_path": "/role-permissions"},
@@ -495,32 +502,9 @@ class RbacAdminServiceImpl(IRbacAdminService):
routeIds = sorted(set(Body.route_ids))
async with GetAsyncSession() as Session:
await self._ensureAdminSeeds(Session)
allRouteIds = [row[0] for row in (await Session.execute(text("SELECT id FROM sys_routes WHERE deleted_at IS NULL AND route_path = ANY(:paths)").bindparams(paths=[item["route_path"] for item in self._MANAGEABLE_ROUTE_BLUEPRINTS]))).fetchall()]
existingRows = (
await Session.execute(text("SELECT route_id, status FROM role_route WHERE role_id = :role_id AND route_id = ANY(:route_ids)").bindparams(route_ids=allRouteIds), {"role_id": RoleId})
).fetchall()
existingMap = {int(routeId): int(status) for routeId, status in existingRows}
insertedCount = 0
for routeId in allRouteIds:
if routeId in routeIds:
if routeId in existingMap:
await Session.execute(
text("UPDATE role_route SET status = 1, permission = :permission, updated_at = NOW() WHERE role_id = :role_id AND route_id = :route_id"),
{"role_id": RoleId, "route_id": routeId, "permission": Body.permission},
)
else:
await Session.execute(
text("INSERT INTO role_route (role_id, route_id, permission, status, created_at, updated_at) VALUES (:role_id, :route_id, :permission, 1, NOW(), NOW())"),
{"role_id": RoleId, "route_id": routeId, "permission": Body.permission},
)
insertedCount += 1
elif routeId in existingMap and existingMap[routeId] != 0:
await Session.execute(
text("UPDATE role_route SET status = 0, updated_at = NOW() WHERE role_id = :role_id AND route_id = :route_id"),
{"role_id": RoleId, "route_id": routeId},
)
result = await self._updateRoleRoutesInSession(Session, RoleId, routeIds, Body.permission)
await Session.commit()
return RoleRouteUpdateResultVO(role_id=RoleId, enabled_count=len(routeIds), disabled_count=max(len(allRouteIds) - len(routeIds), 0), inserted_count=insertedCount, route_ids=routeIds)
return result
async def GetRolePermissions(self, CurrentUserId: int, RoleId: int) -> RolePermissionsVO:
"""查询角色权限授权。"""
@@ -550,29 +534,29 @@ class RbacAdminServiceImpl(IRbacAdminService):
await self._assertPermission(CurrentUserId, "rbac:role_permissions:write")
async with GetAsyncSession() as Session:
await self._ensureAdminSeeds(Session)
permissionIds = [item.permission_id for item in Body.permissions]
if Body.replace:
await Session.execute(text("DELETE FROM role_permissions WHERE role_id = :role_id"), {"role_id": Body.role_id})
for item in Body.permissions:
await Session.execute(
text(
"""
INSERT INTO role_permissions (role_id, permission_id, grant_type, data_scope, created_at, updated_at)
VALUES (:role_id, :permission_id, :grant_type, :data_scope, NOW(), NOW())
ON CONFLICT (role_id, permission_id)
DO UPDATE SET grant_type = EXCLUDED.grant_type, data_scope = EXCLUDED.data_scope, updated_at = NOW()
"""
),
{
"role_id": Body.role_id,
"permission_id": item.permission_id,
"grant_type": item.grant_type,
"data_scope": item.data_scope,
},
)
await self._saveRolePermissionsInSession(Session, Body.role_id, Body.permissions, Body.replace, Body.replace_scope_permission_ids)
await Session.commit()
return await self.GetRolePermissions(CurrentUserId, Body.role_id)
async def SaveRoleAccess(self, CurrentUserId: int, RoleId: int, Body: RoleAccessSaveDTO) -> RoleAccessSaveVO:
"""原子保存角色菜单与接口权限。"""
await self._assertManagePermission(CurrentUserId)
await self._assertPermission(CurrentUserId, "rbac:role_routes:write")
await self._assertPermission(CurrentUserId, "rbac:role_permissions:write")
routeIds = sorted(set(int(routeId) for routeId in Body.route_ids))
permissionIds = sorted(set(int(permissionId) for permissionId in Body.permission_ids))
permissionConfigs = [
{"permission_id": permissionId, "grant_type": "GRANT", "data_scope": "ALL"}
for permissionId in permissionIds
]
async with GetAsyncSession() as Session:
await self._ensureAdminSeeds(Session)
routeResult = await self._updateRoleRoutesInSession(Session, RoleId, routeIds, Body.route_permission)
await self._saveRolePermissionsInSession(Session, RoleId, permissionConfigs, True, Body.replace_scope_permission_ids)
permissionResult = await self._getRolePermissionsInSession(Session, RoleId)
await Session.commit()
return RoleAccessSaveVO(role_id=RoleId, route_result=routeResult, permission_result=permissionResult)
async def GetRoutePermissions(self, CurrentUserId: int, RouteId: int) -> RoutePermissionsVO:
"""查询路由关联权限定义。"""
await self._assertManagePermission(CurrentUserId)
@@ -592,6 +576,104 @@ class RbacAdminServiceImpl(IRbacAdminService):
permissions=permissionMap.get(RouteId, []),
)
async def _updateRoleRoutesInSession(self, Session, RoleId: int, RouteIds: list[int], Permission: str) -> RoleRouteUpdateResultVO:
"""在当前事务中写入角色路由授权。"""
allRouteIds = [
row[0]
for row in (
await Session.execute(
text("SELECT id FROM sys_routes WHERE deleted_at IS NULL AND route_path = ANY(:paths)").bindparams(
paths=[item["route_path"] for item in self._MANAGEABLE_ROUTE_BLUEPRINTS]
)
)
).fetchall()
]
existingRows = (
await Session.execute(
text("SELECT route_id, status FROM role_route WHERE role_id = :role_id AND route_id = ANY(:route_ids)").bindparams(route_ids=allRouteIds),
{"role_id": RoleId},
)
).fetchall()
existingMap = {int(routeId): int(status) for routeId, status in existingRows}
insertedCount = 0
for routeId in allRouteIds:
if routeId in RouteIds:
if routeId in existingMap:
await Session.execute(
text("UPDATE role_route SET status = 1, permission = :permission, updated_at = NOW() WHERE role_id = :role_id AND route_id = :route_id"),
{"role_id": RoleId, "route_id": routeId, "permission": Permission},
)
else:
await Session.execute(
text("INSERT INTO role_route (role_id, route_id, permission, status, created_at, updated_at) VALUES (:role_id, :route_id, :permission, 1, NOW(), NOW())"),
{"role_id": RoleId, "route_id": routeId, "permission": Permission},
)
insertedCount += 1
elif routeId in existingMap and existingMap[routeId] != 0:
await Session.execute(
text("UPDATE role_route SET status = 0, updated_at = NOW() WHERE role_id = :role_id AND route_id = :route_id"),
{"role_id": RoleId, "route_id": routeId},
)
return RoleRouteUpdateResultVO(
role_id=RoleId,
enabled_count=len(RouteIds),
disabled_count=max(len(allRouteIds) - len(RouteIds), 0),
inserted_count=insertedCount,
route_ids=RouteIds,
)
async def _saveRolePermissionsInSession(self, Session, RoleId: int, Permissions: list[Any], Replace: bool, ReplaceScopePermissionIds: list[int]) -> None:
"""在当前事务中写入角色接口权限。"""
if Replace:
scopeIds = sorted({int(permissionId) for permissionId in ReplaceScopePermissionIds if permissionId})
if scopeIds:
# 仅清理当前页面负责维护的权限范围,避免局部页面保存时误删其他模块权限。
await Session.execute(
text("DELETE FROM role_permissions WHERE role_id = :role_id AND permission_id = ANY(:permission_ids)").bindparams(permission_ids=scopeIds),
{"role_id": RoleId},
)
else:
# 兼容旧调用方:若未传作用域,保留原有全量替换行为。
await Session.execute(text("DELETE FROM role_permissions WHERE role_id = :role_id"), {"role_id": RoleId})
for item in Permissions:
permissionId = int(item.permission_id if hasattr(item, "permission_id") else item["permission_id"])
grantType = item.grant_type if hasattr(item, "grant_type") else item.get("grant_type")
dataScope = item.data_scope if hasattr(item, "data_scope") else item.get("data_scope")
await Session.execute(
text(
"""
INSERT INTO role_permissions (role_id, permission_id, grant_type, data_scope, created_at, updated_at)
VALUES (:role_id, :permission_id, :grant_type, :data_scope, NOW(), NOW())
ON CONFLICT (role_id, permission_id)
DO UPDATE SET grant_type = EXCLUDED.grant_type, data_scope = EXCLUDED.data_scope, updated_at = NOW()
"""
),
{
"role_id": RoleId,
"permission_id": permissionId,
"grant_type": grantType,
"data_scope": dataScope,
},
)
async def _getRolePermissionsInSession(self, Session, RoleId: int) -> RolePermissionsVO:
"""在当前事务中查询角色接口权限。"""
rows = (
await Session.execute(
text(
"""
SELECT rp.id, rp.permission_id, p.permission_key, p.display_name, rp.grant_type, rp.data_scope
FROM role_permissions rp
JOIN permissions p ON p.id = rp.permission_id
WHERE rp.role_id = :role_id
ORDER BY p.sort_order ASC, p.id ASC
"""
),
{"role_id": RoleId},
)
).mappings().all()
return RolePermissionsVO(role_id=RoleId, permissions=[self._toRolePermissionVo(row) for row in rows])
async def _assertManagePermission(self, CurrentUserId: int) -> None:
"""校验当前用户是否具备管理能力。"""
context = await self._getCurrentUserContext(CurrentUserId)