"""RBAC 管理服务实现。""" from __future__ import annotations from datetime import datetime from typing import Any from sqlalchemy import text from fastapi_common.fastapi_common_sqlalchemy.database import GetAsyncSession 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 ( OrganizationNodeVO, OrganizationPathVO, OrganizationTreeUserVO, OrganizationTreeVO, RoleAccessSaveVO, RoleListVO, RolePermissionsVO, RoleRoutesVO, RoleRouteUpdateResultVO, RoleVO, RoutePermissionsVO, RoutePermissionVO, RouteVO, UserListVO, UserRoleVO, UserRolesVO, UserVO, ) from fastapi_modules.fastapi_leaudit.services.rbacAdminService import IRbacAdminService class RbacAdminServiceImpl(IRbacAdminService): """RBAC 管理服务实现。""" _MANAGEABLE_ROUTE_BLUEPRINTS: list[dict[str, Any]] = [ { "route_path": "/home", "route_name": "home", "component": "home", "route_title": "系统概览", "icon": "ri-home-line", "sort_order": 1, "is_hidden": False, "is_cache": True, "meta": {"group": "overview"}, }, { "route_path": "/chat-with-llm", "route_name": "chat-with-llm", "component": "chat-with-llm", "route_title": "AI对话", "icon": "ri-chat-smile-2-line", "sort_order": 2, "is_hidden": False, "is_cache": True, "meta": {"group": "assistant"}, }, { "route_path": "/files", "route_name": "file-management", "component": "files", "route_title": "文件管理", "icon": "ri-folder-line", "sort_order": 3, "is_hidden": False, "is_cache": True, "meta": {"group": "documents"}, }, { "route_path": "/files/upload", "route_name": "file-upload", "component": "files.upload", "route_title": "文件上传", "icon": "ri-upload-cloud-line", "sort_order": 1, "parent_path": "/files", "is_hidden": False, "is_cache": True, "meta": {"group": "documents"}, }, { "route_path": "/documents", "route_name": "documents", "component": "documents", "route_title": "文档列表", "icon": "ri-file-list-3-line", "sort_order": 2, "parent_path": "/files", "is_hidden": False, "is_cache": True, "meta": {"group": "documents"}, }, { "route_path": "/cross-checking", "route_name": "cross-checking", "component": "cross-checking", "route_title": "交叉评查", "icon": "ri-color-filter-line", "sort_order": 60, "is_hidden": False, "is_cache": True, "meta": {"group": "cross-review"}, }, { "route_path": "/cross-checking/upload", "route_name": "cross-checking-upload", "component": "cross-checking.upload", "route_title": "创建任务", "icon": "ri-upload-cloud-line", "sort_order": 1, "parent_path": "/cross-checking", "is_hidden": False, "is_cache": True, "meta": {"group": "cross-review"}, }, { "route_path": "/cross-checking/result", "route_name": "cross-checking-result", "component": "cross-checking.result", "route_title": "评查任务列表", "icon": "ri-file-list-3-line", "sort_order": 2, "parent_path": "/cross-checking", "is_hidden": False, "is_cache": True, "meta": {"group": "cross-review"}, }, { "route_path": "/settings", "route_name": "system-settings", "component": "settings", "route_title": "系统设置", "icon": "ri-settings-4-line", "sort_order": 90, "is_hidden": False, "is_cache": True, "meta": {"group": "settings"}, }, { "route_path": "/entry-modules", "route_name": "entry-modules", "component": "entry-modules", "route_title": "入口模块管理", "icon": "ri-apps-2-line", "sort_order": 1, "parent_path": "/settings", "is_hidden": False, "is_cache": True, "meta": {"group": "settings"}, }, { "route_path": "/role-permissions", "route_name": "role-permissions", "component": "role-permissions", "route_title": "角色权限管理", "icon": "ri-shield-user-line", "sort_order": 2, "parent_path": "/settings", "is_hidden": False, "is_cache": True, "meta": {"group": "settings"}, }, { "route_path": "/document-types", "route_name": "document-types", "component": "document-types", "route_title": "文档类型管理", "icon": "ri-file-list-3-line", "sort_order": 3, "parent_path": "/settings", "is_hidden": False, "is_cache": True, "meta": {"group": "settings"}, }, { "route_path": "/usage-stats", "route_name": "usage-stats", "component": "usage-stats", "route_title": "系统使用统计", "icon": "ri-bar-chart-box-line", "sort_order": 4, "parent_path": "/settings", "is_hidden": False, "is_cache": True, "meta": {"group": "settings"}, }, ] _MANAGEABLE_PERMISSION_BLUEPRINTS: list[dict[str, Any]] = [ {"permission_key": "entry_module:list:read", "display_name": "入口模块列表", "module": "entry_module", "resource": "list", "action": "read", "api_method": "GET", "api_path": "/api/v3/entry-modules", "route_path": "/entry-modules"}, {"permission_key": "entry_module:detail:read", "display_name": "入口模块详情", "module": "entry_module", "resource": "detail", "action": "read", "api_method": "GET", "api_path": "/api/v3/entry-modules/{id}", "route_path": "/entry-modules"}, {"permission_key": "entry_module:create:write", "display_name": "创建入口模块", "module": "entry_module", "resource": "create", "action": "write", "api_method": "POST", "api_path": "/api/v3/entry-modules", "route_path": "/entry-modules"}, {"permission_key": "entry_module:update:write", "display_name": "更新入口模块", "module": "entry_module", "resource": "update", "action": "write", "api_method": "PUT", "api_path": "/api/v3/entry-modules/{id}", "route_path": "/entry-modules"}, {"permission_key": "entry_module:delete:delete", "display_name": "删除入口模块", "module": "entry_module", "resource": "delete", "action": "delete", "api_method": "DELETE", "api_path": "/api/v3/entry-modules/{id}", "route_path": "/entry-modules"}, {"permission_key": "entry_module:image:write", "display_name": "上传入口模块图标", "module": "entry_module", "resource": "image", "action": "write", "api_method": "POST", "api_path": "/api/v3/entry-modules/{id}/image", "route_path": "/entry-modules"}, {"permission_key": "doc_type:list:read", "display_name": "文档类型列表", "module": "doc_type", "resource": "list", "action": "read", "api_method": "GET", "api_path": "/api/document-types", "route_path": "/document-types"}, {"permission_key": "doc_type:detail:read", "display_name": "文档类型详情", "module": "doc_type", "resource": "detail", "action": "read", "api_method": "GET", "api_path": "/api/document-types/{id}", "route_path": "/document-types"}, {"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": "usage_stats:overview:read", "display_name": "查看统计总览", "module": "usage_stats", "resource": "overview", "action": "read", "api_method": "GET", "api_path": "/api/v3/usage-stats/overview", "route_path": "/usage-stats"}, {"permission_key": "usage_stats:trends:read", "display_name": "查看统计趋势", "module": "usage_stats", "resource": "trends", "action": "read", "api_method": "GET", "api_path": "/api/v3/usage-stats/trends", "route_path": "/usage-stats"}, {"permission_key": "usage_stats:users:read", "display_name": "查看用户统计", "module": "usage_stats", "resource": "users", "action": "read", "api_method": "GET", "api_path": "/api/v3/usage-stats/by-users", "route_path": "/usage-stats"}, {"permission_key": "usage_stats:departments:read", "display_name": "查看部门统计", "module": "usage_stats", "resource": "departments", "action": "read", "api_method": "GET", "api_path": "/api/v3/usage-stats/by-departments", "route_path": "/usage-stats"}, {"permission_key": "usage_stats:areas:read", "display_name": "查看地区统计", "module": "usage_stats", "resource": "areas", "action": "read", "api_method": "GET", "api_path": "/api/v3/usage-stats/by-areas", "route_path": "/usage-stats"}, {"permission_key": "usage_stats:details:read", "display_name": "查看统计明细", "module": "usage_stats", "resource": "details", "action": "read", "api_method": "GET", "api_path": "/api/v3/usage-stats/details", "route_path": "/usage-stats"}, {"permission_key": "evaluation_group:list:read", "display_name": "评查点分组列表", "module": "evaluation_group", "resource": "list", "action": "read", "api_method": "GET", "api_path": "/api/v3/evaluation-point-groups", "route_path": "/rule-groups"}, {"permission_key": "evaluation_group:create:write", "display_name": "创建评查点分组", "module": "evaluation_group", "resource": "create", "action": "write", "api_method": "POST", "api_path": "/api/v3/evaluation-point-groups", "route_path": "/rule-groups"}, {"permission_key": "evaluation_group:update:write", "display_name": "更新评查点分组与绑定", "module": "evaluation_group", "resource": "update", "action": "write", "api_method": "PUT", "api_path": "/api/v3/evaluation-point-groups/{id}", "route_path": "/rule-groups"}, {"permission_key": "evaluation_group:batch:write", "display_name": "批量维护评查点分组", "module": "evaluation_group", "resource": "batch", "action": "write", "api_method": "PATCH", "api_path": "/api/v3/evaluation-point-groups/batch/status", "route_path": "/rule-groups"}, {"permission_key": "evaluation_group:delete:delete", "display_name": "删除评查点分组", "module": "evaluation_group", "resource": "delete", "action": "delete", "api_method": "DELETE", "api_path": "/api/v3/evaluation-point-groups/{id}", "route_path": "/rule-groups"}, {"permission_key": "rules:list:read", "display_name": "规则配置列表", "module": "rules", "resource": "list", "action": "read", "api_method": "GET", "api_path": "/api/v3/rule-config-packs", "route_path": "/rules"}, {"permission_key": "rules:content:read", "display_name": "规则 YAML 内容", "module": "rules", "resource": "content", "action": "read", "api_method": "GET", "api_path": "/api/v3/rule-config-packs/{id}", "route_path": "/rules"}, {"permission_key": "rules:create:write", "display_name": "创建规则草稿", "module": "rules", "resource": "create", "action": "write", "api_method": "POST", "api_path": "/api/v3/evaluation-point-groups/{id}/rule-drafts", "route_path": "/rules"}, {"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": "cross_review:task:create", "display_name": "创建交叉评查任务", "module": "cross_review", "resource": "task", "action": "create", "api_method": "POST", "api_path": "/api/v3/cross-review/tasks", "route_path": "/cross-checking/upload"}, {"permission_key": "cross_review:task:read", "display_name": "查看交叉评查任务", "module": "cross_review", "resource": "task", "action": "read", "api_method": "POST", "api_path": "/api/v3/cross-review/tasks/query", "route_path": "/cross-checking"}, {"permission_key": "cross_review:progress:view", "display_name": "查看交叉评查任务进度", "module": "cross_review", "resource": "progress", "action": "view", "api_method": "GET", "api_path": "/api/v3/cross-review/tasks/{task_id}/progress", "route_path": "/cross-checking"}, {"permission_key": "cross_review:document:read", "display_name": "查看交叉评查任务文档", "module": "cross_review", "resource": "document", "action": "read", "api_method": "GET", "api_path": "/api/v3/cross-review/tasks/{task_id}/documents", "route_path": "/cross-checking/result"}, {"permission_key": "cross_review:document:complete", "display_name": "确认交叉评查文档完成", "module": "cross_review", "resource": "document", "action": "complete", "api_method": "GET", "api_path": "/api/v3/cross-review/tasks/{task_id}/can-confirm", "route_path": "/cross-checking/result"}, {"permission_key": "cross_review:proposal:create", "display_name": "创建交叉评查提案", "module": "cross_review", "resource": "proposal", "action": "create", "api_method": "POST", "api_path": "/api/v3/cross-review/proposals", "route_path": "/cross-checking/result"}, {"permission_key": "cross_review:proposal:read", "display_name": "查看交叉评查提案", "module": "cross_review", "resource": "proposal", "action": "read", "api_method": "GET", "api_path": "/api/v3/cross-review/documents/{document_id}/proposals", "route_path": "/cross-checking/result"}, {"permission_key": "cross_review:proposal:delete", "display_name": "撤销交叉评查提案", "module": "cross_review", "resource": "proposal", "action": "delete", "api_method": "DELETE", "api_path": "/api/v3/cross-review/proposals/{proposal_id}", "route_path": "/cross-checking/result"}, {"permission_key": "cross_review:proposal:vote", "display_name": "交叉评查提案投票", "module": "cross_review", "resource": "proposal", "action": "vote", "api_method": "POST", "api_path": "/api/v3/cross-review/proposals/{proposal_id}/votes", "route_path": "/cross-checking/result"}, {"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"}, {"permission_key": "rbac:roles:delete", "display_name": "删除角色", "module": "rbac", "resource": "roles", "action": "delete", "api_method": "DELETE", "api_path": "/api/v3/rbac/roles/{role_id}", "route_path": "/role-permissions"}, {"permission_key": "rbac:users:read", "display_name": "用户列表", "module": "rbac", "resource": "users", "action": "read", "api_method": "GET", "api_path": "/api/v3/rbac/users", "route_path": "/role-permissions"}, {"permission_key": "rbac:user_roles:write", "display_name": "分配用户角色", "module": "rbac", "resource": "user_roles", "action": "write", "api_method": "POST", "api_path": "/api/v3/rbac/users/{user_id}/roles", "route_path": "/role-permissions"}, {"permission_key": "rbac:role_routes:write", "display_name": "配置角色菜单", "module": "rbac", "resource": "role_routes", "action": "write", "api_method": "PUT", "api_path": "/api/rbac/roles/{role_id}/routes", "route_path": "/role-permissions"}, {"permission_key": "rbac:role_permissions:write", "display_name": "配置角色权限", "module": "rbac", "resource": "role_permissions", "action": "write", "api_method": "POST", "api_path": "/api/v3/rbac/role-permissions", "route_path": "/role-permissions"}, {"permission_key": "rag:app:read", "display_name": "查看 RAG 应用", "module": "rag", "resource": "app", "action": "read", "api_method": "GET", "api_path": "/api/v3/rag/apps", "route_path": "/chat-with-llm"}, {"permission_key": "rag:chat:use", "display_name": "使用 RAG 对话", "module": "rag", "resource": "chat", "action": "use", "api_method": "POST", "api_path": "/api/v3/rag/chat/messages", "route_path": "/chat-with-llm"}, {"permission_key": "rag:conversation:read", "display_name": "查看 RAG 会话", "module": "rag", "resource": "conversation", "action": "read", "api_method": "GET", "api_path": "/api/v3/rag/chat/conversations", "route_path": "/chat-with-llm"}, {"permission_key": "rag:conversation:update", "display_name": "重命名 RAG 会话", "module": "rag", "resource": "conversation", "action": "update", "api_method": "PATCH", "api_path": "/api/v3/rag/chat/conversations/{ConversationId}", "route_path": "/chat-with-llm"}, {"permission_key": "rag:conversation:delete", "display_name": "删除 RAG 会话", "module": "rag", "resource": "conversation", "action": "delete", "api_method": "DELETE", "api_path": "/api/v3/rag/chat/conversations/{ConversationId}", "route_path": "/chat-with-llm"}, {"permission_key": "rag:message:feedback", "display_name": "反馈 RAG 消息", "module": "rag", "resource": "message", "action": "feedback", "api_method": "POST", "api_path": "/api/v3/rag/chat/messages/{MessageId}/feedback", "route_path": "/chat-with-llm"}, {"permission_key": "rag:dataset:read", "display_name": "查看 RAG 知识库", "module": "rag", "resource": "dataset", "action": "read", "api_method": "GET", "api_path": "/api/v3/rag/datasets/my", "route_path": "/chat-with-llm"}, {"permission_key": "rag:dataset:manage", "display_name": "查看知识库配置管理", "module": "rag", "resource": "dataset", "action": "manage", "api_method": "GET", "api_path": "/api/v3/rag/datasets/admin", "route_path": "/chat-with-llm"}, {"permission_key": "rag:dataset:create", "display_name": "创建知识库", "module": "rag", "resource": "dataset", "action": "create", "api_method": "POST", "api_path": "/api/v3/rag/datasets/admin", "route_path": "/chat-with-llm"}, {"permission_key": "rag:dataset:update", "display_name": "更新知识库与文档", "module": "rag", "resource": "dataset", "action": "update", "api_method": "PATCH", "api_path": "/api/v3/rag/datasets/{DatasetId}", "route_path": "/chat-with-llm"}, {"permission_key": "rag:dataset:delete", "display_name": "删除知识库与文档", "module": "rag", "resource": "dataset", "action": "delete", "api_method": "DELETE", "api_path": "/api/v3/rag/datasets/admin/{DatasetId}", "route_path": "/chat-with-llm"}, ] async def ListRoles(self, CurrentUserId: int, Page: int, PageSize: int, RoleKey: str | None, RoleName: str | None, IncludeSystem: bool) -> RoleListVO: """查询角色列表。""" await self._assertManagePermission(CurrentUserId) await self._assertPermission(CurrentUserId, "rbac:roles:read") offset = max(Page - 1, 0) * PageSize filters = ["1=1"] params: dict[str, object] = {"limit": PageSize, "offset": offset} if RoleKey: filters.append("role_key ILIKE :role_key") params["role_key"] = f"%{RoleKey.strip()}%" if RoleName: filters.append("role_name ILIKE :role_name") params["role_name"] = f"%{RoleName.strip()}%" if not IncludeSystem: filters.append("is_system_role = FALSE") whereClause = " AND ".join(filters) async with GetAsyncSession() as Session: total = int((await Session.execute(text(f"SELECT COUNT(*) FROM roles WHERE {whereClause}"), params)).scalar_one()) rows = ( await Session.execute( text( f""" SELECT id, role_key, role_name, data_scope, description, parent_role_id, priority, is_system_role, created_at, updated_at FROM roles WHERE {whereClause} ORDER BY priority DESC, id ASC LIMIT :limit OFFSET :offset """ ), params, ) ).mappings().all() return RoleListVO(total=total, page=Page, page_size=PageSize, items=[self._toRoleVo(row) for row in rows]) async def CreateRole(self, CurrentUserId: int, Body: RoleCreateDTO) -> RoleVO: """创建角色。""" await self._assertManagePermission(CurrentUserId) await self._assertPermission(CurrentUserId, "rbac:roles:create") async with GetAsyncSession() as Session: row = ( await Session.execute( text( """ INSERT INTO roles (role_key, role_name, data_scope, description, priority, is_system_role, metadata, created_at, updated_at) VALUES (:role_key, :role_name, :data_scope, :description, 0, FALSE, CAST(:metadata AS jsonb), NOW(), NOW()) RETURNING id, role_key, role_name, data_scope, description, parent_role_id, priority, is_system_role, created_at, updated_at """ ), { "role_key": Body.role_key.strip(), "role_name": Body.role_name.strip(), "data_scope": Body.data_scope, "description": (Body.description or "").strip(), "metadata": self._jsonDump(Body.metadata or {}), }, ) ).mappings().one() await Session.commit() return self._toRoleVo(row) async def UpdateRole(self, CurrentUserId: int, RoleId: int, Body: RoleUpdateDTO) -> RoleVO: """更新角色。""" await self._assertManagePermission(CurrentUserId) await self._assertPermission(CurrentUserId, "rbac:roles:update") async with GetAsyncSession() as Session: current = await self._getRoleRow(Session, RoleId) row = ( await Session.execute( text( """ UPDATE roles SET role_name = :role_name, description = :description, data_scope = :data_scope, priority = :priority, parent_role_id = :parent_role_id, updated_at = NOW() WHERE id = :role_id RETURNING id, role_key, role_name, data_scope, description, parent_role_id, priority, is_system_role, created_at, updated_at """ ), { "role_id": RoleId, "role_name": Body.role_name if Body.role_name is not None else current["role_name"], "description": Body.description if Body.description is not None else current["description"], "data_scope": Body.data_scope if Body.data_scope is not None else current["data_scope"], "priority": Body.priority if Body.priority is not None else current["priority"], "parent_role_id": Body.parent_role_id if Body.parent_role_id is not None else current["parent_role_id"], }, ) ).mappings().one() await Session.commit() return self._toRoleVo(row) async def DeleteRole(self, CurrentUserId: int, RoleId: int, Force: bool) -> None: """删除角色。""" await self._assertManagePermission(CurrentUserId) await self._assertPermission(CurrentUserId, "rbac:roles:delete") async with GetAsyncSession() as Session: role = await self._getRoleRow(Session, RoleId) if role["is_system_role"]: raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "系统内置角色不允许删除") userCount = int((await Session.execute(text("SELECT COUNT(*) FROM user_role WHERE role_id = :role_id"), {"role_id": RoleId})).scalar_one()) if userCount > 0 and not Force: raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "角色仍绑定用户,请传 force=true 后重试") if Force: await Session.execute(text("DELETE FROM user_role WHERE role_id = :role_id"), {"role_id": RoleId}) await Session.execute(text("DELETE FROM roles WHERE id = :role_id"), {"role_id": RoleId}) await Session.commit() async def ListUsers(self, CurrentUserId: int, Page: int, PageSize: int, Area: str | None, NickName: str | None) -> UserListVO: """查询用户列表。""" await self._assertManagePermission(CurrentUserId) await self._assertPermission(CurrentUserId, "rbac:users:read") currentUser = await self._getCurrentUserContext(CurrentUserId) offset = max(Page - 1, 0) * PageSize filters = ["u.deleted_at IS NULL", "u.status = 0"] params: dict[str, object] = {"limit": PageSize, "offset": offset} if NickName: filters.append("u.nick_name ILIKE :nick_name") params["nick_name"] = f"%{NickName.strip()}%" if Area: filters.append("u.area = :query_area") params["query_area"] = Area.strip() elif not currentUser["is_global"]: filters.append("COALESCE(u.area, '') = :user_area") params["user_area"] = currentUser["area"] whereClause = " AND ".join(filters) async with GetAsyncSession() as Session: total = int((await Session.execute(text(f"SELECT COUNT(*) FROM sso_users u WHERE {whereClause}"), params)).scalar_one()) rows = ( await Session.execute( text( f""" SELECT u.id, u.username, u.nick_name, u.phone_number, u.email, u.area, u.ou_name, u.ou_id, u.status, u.is_leader, u.tenant_name, u.dep_name, u.dep_short_name, COALESCE( json_agg( DISTINCT jsonb_build_object('role_id', r.id, 'role_key', r.role_key, 'role_name', r.role_name) ) FILTER (WHERE r.id IS NOT NULL), '[]'::json ) AS roles FROM sso_users u LEFT JOIN user_role ur ON ur.user_id = u.id LEFT JOIN roles r ON r.id = ur.role_id WHERE {whereClause} GROUP BY u.id ORDER BY u.created_at DESC, u.id DESC LIMIT :limit OFFSET :offset """ ), params, ) ).mappings().all() return UserListVO(total=total, page=Page, page_size=PageSize, items=[self._toUserVo(row) for row in rows]) async def GetOrganizationTree(self, CurrentUserId: int, IncludeUsers: bool, RootUuid: str | None) -> OrganizationTreeVO: """查询组织树。""" await self._assertManagePermission(CurrentUserId) await self._assertPermission(CurrentUserId, "rbac:users:read") currentUser = await self._getCurrentUserContext(CurrentUserId) user_filters = ["deleted_at IS NULL", "status = 0"] params: dict[str, object] = {} if not currentUser["is_global"]: user_filters.append("COALESCE(area, '') = :user_area") params["user_area"] = currentUser["area"] where_clause = " AND ".join(user_filters) async with GetAsyncSession() as Session: user_rows = ( await Session.execute( text( f""" SELECT id, username, nick_name, area, ou_id, ou_name, is_leader, status, tenant_name, dep_name, dep_short_name, email, phone_number FROM sso_users WHERE {where_clause} ORDER BY COALESCE(tenant_name, '') ASC, COALESCE(dep_name, '') ASC, COALESCE(ou_name, '') ASC, id ASC """ ), params, ) ).mappings().all() root_uuid = str(RootUuid or "").strip() or None tenant_nodes: dict[str, OrganizationNodeVO] = {} department_nodes: dict[str, OrganizationNodeVO] = {} organization_nodes: dict[str, OrganizationNodeVO] = {} department_children: dict[str, list[str]] = {} organization_children: dict[str, list[str]] = {} users_by_org: dict[str, list[OrganizationTreeUserVO]] = {} total_users = len(user_rows) for row in user_rows: ou_id = str(row.get("ou_id") or "").strip() ou_name = str(row.get("ou_name") or "").strip() or ou_id tenant_name = str(row.get("tenant_name") or "").strip() dep_name = str(row.get("dep_name") or "").strip() dep_short_name = str(row.get("dep_short_name") or "").strip() tenant_label = tenant_name or "未分组租户" department_label = dep_name or dep_short_name or "未分组部门" organization_label = ou_name or "未分组组织" tenant_key = f"tenant__{tenant_label}" dep_key = f"dep__{tenant_label}__{department_label}" org_key = ou_id or f"org__{tenant_label}__{department_label}__{organization_label}" if tenant_key not in tenant_nodes: tenant_nodes[tenant_key] = OrganizationNodeVO( ou_id=tenant_key, ou_name=tenant_label, parent_ou_id=None, level=1, ) if dep_key not in department_nodes: department_nodes[dep_key] = OrganizationNodeVO( ou_id=dep_key, ou_name=department_label, parent_ou_id=tenant_key, level=2, ) department_children.setdefault(tenant_key, []).append(dep_key) if org_key not in organization_nodes: organization_nodes[org_key] = OrganizationNodeVO( ou_id=org_key, ou_name=organization_label, parent_ou_id=dep_key, level=3, ) organization_children.setdefault(dep_key, []).append(org_key) if IncludeUsers and ou_id: users_by_org.setdefault(org_key, []).append( OrganizationTreeUserVO( id=int(row["id"]), username=str(row.get("username") or ""), nick_name=str(row.get("nick_name") or ""), area=row.get("area"), ou_id=ou_id, ou_name=ou_name, is_leader=bool(row.get("is_leader", False)), status=int(row.get("status") or 0), tenant_name=row.get("tenant_name"), dep_name=row.get("dep_name"), dep_short_name=row.get("dep_short_name"), email=row.get("email"), phone_number=row.get("phone_number"), organization_path=OrganizationPathVO( tenant_name=tenant_name, dep_name=dep_name, dep_short_name=dep_short_name, ou_name=ou_name, ), ) ) for org_key, users in users_by_org.items(): users.sort(key=lambda item: (not item.is_leader, item.nick_name, item.username, item.id)) node = organization_nodes.get(org_key) if node: node.users = users def clone_node(source: OrganizationNodeVO, *, children: list[OrganizationNodeVO] | None = None, users: list[OrganizationTreeUserVO] | None = None) -> OrganizationNodeVO: return OrganizationNodeVO( ou_id=source.ou_id, ou_name=source.ou_name, parent_ou_id=source.parent_ou_id, level=source.level, children=children or [], users=users or [], ) if root_uuid: if root_uuid in tenant_nodes: tenant = tenant_nodes[root_uuid] child_nodes = [ clone_node(department_nodes[dep_id]) for dep_id in sorted(set(department_children.get(root_uuid, [])), key=lambda item: (department_nodes[item].ou_name, item)) ] organizations = [clone_node(tenant, children=child_nodes)] total_organizations = 1 + len(child_nodes) elif root_uuid in department_nodes: department = department_nodes[root_uuid] child_nodes = [ clone_node(organization_nodes[org_id]) for org_id in sorted(set(organization_children.get(root_uuid, [])), key=lambda item: (organization_nodes[item].ou_name, item)) ] organizations = [clone_node(department, children=child_nodes)] total_organizations = 1 + len(child_nodes) elif root_uuid in organization_nodes: organization = organization_nodes[root_uuid] organization_users = list(users_by_org.get(root_uuid, [])) if IncludeUsers else [] organizations = [clone_node(organization, users=organization_users)] total_organizations = 1 total_users = len(organization_users) if IncludeUsers else total_users else: organizations = [] total_organizations = 0 total_users = 0 else: organizations = [ clone_node(tenant_nodes[tenant_id]) for tenant_id in sorted(tenant_nodes.keys(), key=lambda item: (tenant_nodes[item].ou_name, item)) ] total_organizations = len(organizations) return OrganizationTreeVO( organizations=organizations, total_organizations=total_organizations, total_users=total_users, ) async def ListRoleUsers(self, CurrentUserId: int, RoleId: int, Page: int, PageSize: int, Area: str | None, UserName: str | None) -> UserListVO: """查询指定角色下的用户列表。""" await self._assertManagePermission(CurrentUserId) await self._assertPermission(CurrentUserId, "rbac:users:read") currentUser = await self._getCurrentUserContext(CurrentUserId) offset = max(Page - 1, 0) * PageSize filters = ["u.deleted_at IS NULL", "u.status = 0", "ur.role_id = :role_id"] params: dict[str, object] = {"role_id": RoleId, "limit": PageSize, "offset": offset} if UserName: filters.append("(u.username ILIKE :user_name OR u.nick_name ILIKE :user_name)") params["user_name"] = f"%{UserName.strip()}%" if Area: filters.append("u.area = :query_area") params["query_area"] = Area.strip() elif not currentUser["is_global"]: filters.append("COALESCE(u.area, '') = :user_area") params["user_area"] = currentUser["area"] whereClause = " AND ".join(filters) async with GetAsyncSession() as Session: total = int( ( await Session.execute( text( f""" SELECT COUNT(DISTINCT u.id) FROM sso_users u JOIN user_role ur ON ur.user_id = u.id WHERE {whereClause} """ ), params, ) ).scalar_one() ) rows = ( await Session.execute( text( f""" SELECT u.id, u.username, u.nick_name, u.phone_number, u.email, u.area, u.ou_name, u.ou_id, u.status, u.is_leader, u.tenant_name, u.dep_name, u.dep_short_name, COALESCE( json_agg( DISTINCT jsonb_build_object('role_id', r.id, 'role_key', r.role_key, 'role_name', r.role_name) ) FILTER (WHERE r.id IS NOT NULL), '[]'::json ) AS roles FROM sso_users u JOIN user_role ur ON ur.user_id = u.id LEFT JOIN user_role all_ur ON all_ur.user_id = u.id LEFT JOIN roles r ON r.id = all_ur.role_id WHERE {whereClause} GROUP BY u.id ORDER BY u.created_at DESC, u.id DESC LIMIT :limit OFFSET :offset """ ), params, ) ).mappings().all() return UserListVO(total=total, page=Page, page_size=PageSize, items=[self._toUserVo(row) for row in rows]) async def AssignUserRoles(self, CurrentUserId: int, UserId: int, RoleIds: list[int]) -> UserRolesVO: """为用户分配角色。""" await self._assertManagePermission(CurrentUserId) await self._assertPermission(CurrentUserId, "rbac:user_roles:write") async with GetAsyncSession() as Session: await Session.execute(text("DELETE FROM user_role WHERE user_id = :user_id"), {"user_id": UserId}) for roleId in sorted(set(RoleIds)): await Session.execute( text( "INSERT INTO user_role (user_id, role_id, created_at, updated_at) VALUES (:user_id, :role_id, NOW(), NOW())" ), {"user_id": UserId, "role_id": roleId}, ) await Session.commit() return await self.GetUserRoles(CurrentUserId, UserId) async def RevokeUserRole(self, CurrentUserId: int, UserId: int, RoleId: int) -> None: """移除用户角色。""" await self._assertManagePermission(CurrentUserId) await self._assertPermission(CurrentUserId, "rbac:user_roles:write") async with GetAsyncSession() as Session: await Session.execute(text("DELETE FROM user_role WHERE user_id = :user_id AND role_id = :role_id"), {"user_id": UserId, "role_id": RoleId}) await Session.commit() async def GetUserRoles(self, CurrentUserId: int, UserId: int) -> UserRolesVO: """查询用户角色。""" await self._assertManagePermission(CurrentUserId) await self._assertPermission(CurrentUserId, "rbac:users:read") async with GetAsyncSession() as Session: userRow = ( await Session.execute(text("SELECT id, username FROM sso_users WHERE id = :user_id"), {"user_id": UserId}) ).mappings().first() if not userRow: raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "用户不存在") roleRows = ( await Session.execute( text( """ SELECT r.id, r.role_key, r.role_name, r.data_scope, r.description, r.parent_role_id, r.priority, r.is_system_role, r.created_at, r.updated_at FROM user_role ur JOIN roles r ON r.id = ur.role_id WHERE ur.user_id = :user_id ORDER BY r.priority DESC, r.id ASC """ ), {"user_id": UserId}, ) ).mappings().all() return UserRolesVO(user_id=UserId, username=str(userRow["username"] or ""), roles=[self._toRoleVo(row) for row in roleRows]) async def ListAllRoutes(self, CurrentUserId: int, Format: str, IncludeHidden: bool) -> list[RouteVO]: """查询全部可管理路由。""" await self._assertManagePermission(CurrentUserId) async with GetAsyncSession() as Session: routeMap = await self._ensureAdminSeeds(Session) rows = ( await Session.execute( text( """ SELECT id, route_path, route_name, component, parent_id, route_title, icon, sort_order, is_hidden, is_cache, status FROM sys_routes WHERE deleted_at IS NULL AND route_path = ANY(:paths) ORDER BY sort_order ASC, id ASC """ ).bindparams(paths=list(routeMap.keys())), ) ).mappings().all() permissionMap = await self._loadPermissionsByRoute(Session, [int(row["id"]) for row in rows]) routeVos = [self._toRouteVo(row, permissionMap.get(int(row["id"]), []), False) for row in rows if IncludeHidden or not bool(row["is_hidden"])] return routeVos if Format == "flat" else self._buildRouteTree(routeVos) async def GetRoleRoutes(self, CurrentUserId: int, RoleId: int) -> RoleRoutesVO: """查询角色路由授权。""" await self._assertManagePermission(CurrentUserId) await self._assertPermission(CurrentUserId, "rbac:roles:read") async with GetAsyncSession() as Session: routeMap = await self._ensureAdminSeeds(Session) rows = ( await Session.execute( text( """ SELECT sr.id, sr.route_path, sr.route_name, sr.component, sr.parent_id, sr.route_title, sr.icon, sr.sort_order, sr.is_hidden, sr.is_cache, sr.status, COALESCE(rr.status, 0) AS enabled FROM sys_routes sr LEFT JOIN role_route rr ON rr.route_id = sr.id AND rr.role_id = :role_id WHERE sr.deleted_at IS NULL AND sr.route_path = ANY(:paths) ORDER BY sr.sort_order ASC, sr.id ASC """ ).bindparams(paths=list(routeMap.keys())), {"role_id": RoleId}, ) ).mappings().all() permissionMap = await self._loadPermissionsByRoute(Session, [int(row["id"]) for row in rows]) routeVos = [self._toRouteVo(row, permissionMap.get(int(row["id"]), []), bool(row["enabled"])) for row in rows] return RoleRoutesVO(role_id=RoleId, routes=self._buildRouteTree(routeVos)) async def UpdateRoleRoutes(self, CurrentUserId: int, RoleId: int, Body: RoleRoutesUpdateDTO) -> RoleRouteUpdateResultVO: """更新角色路由授权。""" await self._assertManagePermission(CurrentUserId) await self._assertPermission(CurrentUserId, "rbac:role_routes:write") routeIds = sorted(set(Body.route_ids)) async with GetAsyncSession() as Session: await self._ensureAdminSeeds(Session) result = await self._updateRoleRoutesInSession(Session, RoleId, routeIds, Body.permission) await Session.commit() return result async def GetRolePermissions(self, CurrentUserId: int, RoleId: int) -> RolePermissionsVO: """查询角色权限授权。""" await self._assertManagePermission(CurrentUserId) await self._assertPermission(CurrentUserId, "rbac:roles:read") async with GetAsyncSession() as Session: await self._ensureAdminSeeds(Session) 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 SaveRolePermissions(self, CurrentUserId: int, Body: RolePermissionsBatchDTO) -> RolePermissionsVO: """保存角色权限授权。""" await self._assertManagePermission(CurrentUserId) await self._assertPermission(CurrentUserId, "rbac:role_permissions:write") async with GetAsyncSession() as Session: await self._ensureAdminSeeds(Session) 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) await self._assertPermission(CurrentUserId, "rbac:permissions:read") async with GetAsyncSession() as Session: await self._ensureAdminSeeds(Session) routeRow = ( await Session.execute(text("SELECT id, route_path, route_title FROM sys_routes WHERE id = :route_id"), {"route_id": RouteId}) ).mappings().first() if not routeRow: raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "路由不存在") permissionMap = await self._loadPermissionsByRoute(Session, [RouteId]) return RoutePermissionsVO( route_id=RouteId, route_path=str(routeRow["route_path"] or ""), route_title=str(routeRow["route_title"] or ""), 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) if not context["can_manage"]: raise LeauditException(StatusCodeEnum.HTTP_403_FORBIDDEN, "当前用户没有系统设置管理权限") async def _assertPermission(self, CurrentUserId: int, PermissionKey: str) -> None: """校验当前用户是否具备特定细粒度权限。super_admin 自动放行。""" context = await self._getCurrentUserContext(CurrentUserId) if context["is_super_admin"]: return async with GetAsyncSession() as Session: row = ( await Session.execute( text( """ SELECT p.display_name FROM role_permissions rp JOIN permissions p ON p.id = rp.permission_id JOIN user_role ur ON ur.role_id = rp.role_id WHERE ur.user_id = :user_id AND p.permission_key = :permission_key AND rp.grant_type = 'GRANT' LIMIT 1 """ ), {"user_id": CurrentUserId, "permission_key": PermissionKey}, ) ).mappings().first() if not row: displayRow = ( await Session.execute( text("SELECT display_name FROM permissions WHERE permission_key = :key LIMIT 1"), {"key": PermissionKey}, ) ).mappings().first() displayName = displayRow["display_name"] if displayRow else PermissionKey raise LeauditException(StatusCodeEnum.HTTP_403_FORBIDDEN, f"缺少「{displayName}」权限") async def _getCurrentUserContext(self, CurrentUserId: int) -> dict[str, Any]: """加载当前用户上下文。""" async with GetAsyncSession() as Session: row = ( await Session.execute( text( """ SELECT u.id, COALESCE(u.area, '') AS area, COALESCE(bool_or(r.role_key IN ('super_admin', 'provincial_admin')), FALSE) AS is_global, COALESCE(bool_or(r.role_key IN ('super_admin', 'provincial_admin', 'admin')), FALSE) AS can_manage, COALESCE(bool_or(r.role_key = 'super_admin'), FALSE) AS is_super_admin FROM sso_users u LEFT JOIN user_role ur ON ur.user_id = u.id LEFT JOIN roles r ON r.id = ur.role_id WHERE u.id = :user_id GROUP BY u.id, u.area """ ), {"user_id": CurrentUserId}, ) ).mappings().first() if not row: raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "当前用户不存在") return {"area": str(row["area"] or ""), "is_global": bool(row["is_global"]), "can_manage": bool(row["can_manage"]), "is_super_admin": bool(row["is_super_admin"])} async def _ensureAdminSeeds(self, Session) -> dict[str, int]: """确保系统设置所需路由和权限定义已存在。""" routeIdsByPath: dict[str, int] = {} for blueprint in self._MANAGEABLE_ROUTE_BLUEPRINTS: parentId = routeIdsByPath.get(str(blueprint.get("parent_path"))) if blueprint.get("parent_path") else None row = ( await Session.execute( text( """ INSERT INTO sys_routes (route_path, route_name, component, parent_id, route_title, icon, sort_order, is_hidden, is_cache, meta, status, created_at, updated_at, deleted_at) VALUES (:route_path, :route_name, :component, :parent_id, :route_title, :icon, :sort_order, :is_hidden, :is_cache, CAST(:meta AS jsonb), 0, NOW(), NOW(), NULL) ON CONFLICT (route_path) WHERE deleted_at IS NULL DO UPDATE SET route_name = EXCLUDED.route_name, component = EXCLUDED.component, parent_id = EXCLUDED.parent_id, route_title = EXCLUDED.route_title, icon = EXCLUDED.icon, sort_order = EXCLUDED.sort_order, is_hidden = EXCLUDED.is_hidden, is_cache = EXCLUDED.is_cache, meta = EXCLUDED.meta, status = 0, updated_at = NOW() RETURNING id """ ), { "route_path": blueprint["route_path"], "route_name": blueprint["route_name"], "component": blueprint.get("component"), "parent_id": parentId, "route_title": blueprint["route_title"], "icon": blueprint.get("icon"), "sort_order": blueprint.get("sort_order", 0), "is_hidden": bool(blueprint.get("is_hidden", False)), "is_cache": bool(blueprint.get("is_cache", True)), "meta": self._jsonDump(blueprint.get("meta") or {}), }, ) ).scalar_one() routeIdsByPath[str(blueprint["route_path"])] = int(row) for blueprint in self._MANAGEABLE_PERMISSION_BLUEPRINTS: routeId = routeIdsByPath.get(str(blueprint["route_path"])) await Session.execute( text( """ INSERT INTO permissions ( permission_key, module, resource, action, description, display_name, permission_type, is_system, metadata, created_at, updated_at, parent_id, sort_order, route_id, api_path, api_method, related_routes ) VALUES ( :permission_key, :module, :resource, :action, :description, :display_name, 'API', TRUE, NULL, NOW(), NOW(), NULL, 0, :route_id, :api_path, :api_method, NULL ) ON CONFLICT (permission_key) DO UPDATE SET module = EXCLUDED.module, resource = EXCLUDED.resource, action = EXCLUDED.action, display_name = EXCLUDED.display_name, route_id = EXCLUDED.route_id, api_path = EXCLUDED.api_path, api_method = EXCLUDED.api_method, updated_at = NOW() """ ), { "permission_key": blueprint["permission_key"], "module": blueprint["module"], "resource": blueprint["resource"], "action": blueprint["action"], "description": blueprint["display_name"], "display_name": blueprint["display_name"], "route_id": routeId, "api_path": blueprint["api_path"], "api_method": blueprint["api_method"], }, ) await Session.commit() return routeIdsByPath async def _loadPermissionsByRoute(self, Session, RouteIds: list[int]) -> dict[int, list[RoutePermissionVO]]: """加载路由关联权限定义。""" if not RouteIds: return {} rows = ( await Session.execute( text( """ SELECT id, permission_key, display_name, api_method, api_path, route_id, related_routes FROM permissions WHERE route_id = ANY(:route_ids) OR EXISTS ( SELECT 1 FROM unnest(COALESCE(related_routes, ARRAY[]::bigint[])) AS related_route_id WHERE related_route_id = ANY(:route_ids) ) ORDER BY sort_order ASC, id ASC """ ).bindparams(route_ids=RouteIds), ) ).mappings().all() output: dict[int, list[RoutePermissionVO]] = {int(routeId): [] for routeId in RouteIds} for row in rows: relatedRoutes = [int(item) for item in list(row.get("related_routes") or [])] targetRouteIds = [] if row.get("route_id") is not None: targetRouteIds.append(int(row["route_id"])) targetRouteIds.extend(relatedRoutes) permissionVo = RoutePermissionVO( id=int(row["id"]), permission_key=str(row["permission_key"] or ""), display_name=row.get("display_name"), api_method=row.get("api_method"), api_path=row.get("api_path"), route_id=int(row["route_id"]) if row.get("route_id") is not None else None, related_routes=relatedRoutes or None, is_shared=bool(relatedRoutes), ) for routeId in targetRouteIds: if routeId in output: output[routeId].append(permissionVo) return output async def _getRoleRow(self, Session, RoleId: int): """查询角色原始记录。""" row = ( await Session.execute( text( "SELECT id, role_key, role_name, data_scope, description, parent_role_id, priority, is_system_role, created_at, updated_at FROM roles WHERE id = :role_id" ), {"role_id": RoleId}, ) ).mappings().first() if not row: raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "角色不存在") return row def _toRoleVo(self, Row) -> RoleVO: """角色记录转 VO。""" return RoleVO( id=int(Row["id"]), role_key=str(Row["role_key"] or ""), role_name=str(Row["role_name"] or ""), data_scope=str(Row["data_scope"] or "SELF"), description=str(Row.get("description") or ""), parent_role_id=int(Row["parent_role_id"]) if Row.get("parent_role_id") is not None else None, priority=int(Row.get("priority") or 0), is_system=bool(Row.get("is_system_role", False)), created_at=self._toIso(Row.get("created_at")), updated_at=self._toIso(Row.get("updated_at")), ) def _toUserVo(self, Row) -> UserVO: """用户记录转 VO。""" roles = [] for item in list(Row.get("roles") or []): if isinstance(item, dict) and item.get("role_id") is not None: roles.append(UserRoleVO(role_id=int(item["role_id"]), role_key=str(item.get("role_key") or ""), role_name=str(item.get("role_name") or ""))) return UserVO( id=int(Row["id"]), username=str(Row.get("username") or ""), nick_name=str(Row.get("nick_name") or ""), phone_number=Row.get("phone_number"), email=Row.get("email"), area=Row.get("area"), ou_name=Row.get("ou_name"), ou_id=Row.get("ou_id"), status=int(Row.get("status") or 0), is_leader=bool(Row.get("is_leader", False)), roles=roles, tenant_name=Row.get("tenant_name"), dep_name=Row.get("dep_name"), dep_short_name=Row.get("dep_short_name"), ) def _toRouteVo(self, Row, Permissions: list[RoutePermissionVO], Enabled: bool) -> RouteVO: """路由记录转 VO。""" return RouteVO( id=int(Row["id"]), route_path=str(Row.get("route_path") or ""), route_name=str(Row.get("route_name") or ""), route_title=str(Row.get("route_title") or ""), component=Row.get("component"), parent_id=int(Row["parent_id"]) if Row.get("parent_id") is not None else None, icon=Row.get("icon"), sort_order=int(Row.get("sort_order") or 0), is_hidden=bool(Row.get("is_hidden", False)), is_cache=bool(Row.get("is_cache", True)), status=int(Row.get("status") or 0), enabled=Enabled, permissions=Permissions, children=None, ) def _toRolePermissionVo(self, Row): """角色权限记录转 VO。""" from fastapi_modules.fastapi_leaudit.domian.vo.rbacAdminVo import RolePermissionDetailVO return RolePermissionDetailVO( id=int(Row["id"]), permission_id=int(Row["permission_id"]), permission_key=str(Row["permission_key"] or ""), display_name=Row.get("display_name"), grant_type=str(Row.get("grant_type") or "GRANT"), data_scope=Row.get("data_scope"), ) def _buildRouteTree(self, Routes: list[RouteVO]) -> list[RouteVO]: """构建路由树。""" routeMap = {route.id: route.model_copy(deep=True) for route in Routes} rootRoutes: list[RouteVO] = [] for route in routeMap.values(): route.children = [] for route in routeMap.values(): if route.parent_id and route.parent_id in routeMap: routeMap[route.parent_id].children.append(route) else: rootRoutes.append(route) for route in routeMap.values(): if route.children is not None and len(route.children) == 0: route.children = None elif route.children: route.children.sort(key=lambda item: (item.sort_order, item.id)) rootRoutes.sort(key=lambda item: (item.sort_order, item.id)) return rootRoutes def _jsonDump(self, Value: Any) -> str: """JSON 序列化。""" import json return json.dumps(Value, ensure_ascii=False) def _toIso(self, Value) -> str | None: """时间转 ISO 字符串。""" if Value is None: return None if isinstance(Value, datetime): return Value.isoformat() return str(Value)