Files
leaudit-platform-backend/fastapi_modules/fastapi_leaudit/services/impl/rbacAdminServiceImpl.py
T

1934 lines
107 KiB
Python

"""RBAC 管理服务实现。"""
from __future__ import annotations
import logging
import time
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,
UserTenantUpdateDTO,
)
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,
UserTenantUpdateVO,
UserVO,
)
from fastapi_modules.fastapi_leaudit.services.impl.ssoUserCompat import SsoUserCompat
from fastapi_modules.fastapi_leaudit.services.impl.tenantResolver import TenantResolver
from fastapi_modules.fastapi_leaudit.services.impl.permissionServiceImpl import PermissionServiceImpl
from fastapi_modules.fastapi_leaudit.services.rbacAdminService import IRbacAdminService
class RbacAdminServiceImpl(IRbacAdminService):
"""RBAC 管理服务实现。"""
_logger = logging.getLogger("APP")
_ADMIN_SEED_CACHE_TTL_SECONDS = 300
_admin_seed_route_map_cache: dict[str, int] | None = None
_admin_seed_route_map_cached_at = 0.0
_UNGROUPED_TENANT_LABEL = "未分组租户"
_UNGROUPED_DEPARTMENT_LABEL = "未分组部门"
_UNGROUPED_ORGANIZATION_LABEL = "未分组组织"
_CORE_ROLE_AUTO_ROUTE_PATHS: dict[str, tuple[str, ...]] = {
"super_admin": ("/settings", "/tenants"),
"provincial_admin": ("/settings", "/tenants"),
"admin": ("/settings", "/tenants"),
}
def __init__(self, TenantResolverService: TenantResolver | None = None) -> None:
self.TenantResolver = TenantResolverService or TenantResolver()
_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": "/contract-template",
"route_name": "contract-template",
"component": "contract-template",
"route_title": "合同管理",
"icon": "ri-file-search-line",
"sort_order": 50,
"is_hidden": False,
"is_cache": True,
"meta": {"group": "contract"},
},
{
"route_path": "/contract-template/search",
"route_name": "contract-template-search",
"component": "contract-template.search",
"route_title": "模板搜索",
"icon": "ri-search-line",
"sort_order": 1,
"parent_path": "/contract-template",
"is_hidden": False,
"is_cache": True,
"meta": {"group": "contract"},
},
{
"route_path": "/contract-template/list",
"route_name": "contract-template-list",
"component": "contract-template.list",
"route_title": "模板列表",
"icon": "ri-folder-line",
"sort_order": 2,
"parent_path": "/contract-template",
"is_hidden": False,
"is_cache": True,
"meta": {"group": "contract"},
},
{
"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": "/rules",
"route_name": "rule-management",
"component": "rules",
"route_title": "规则管理",
"icon": "ri-book-3-line",
"sort_order": 70,
"is_hidden": False,
"is_cache": True,
"meta": {"group": "rules"},
},
{
"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": "/tenants",
"route_name": "tenants",
"component": "tenants",
"route_title": "租户管理",
"icon": "ri-building-line",
"sort_order": 4,
"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": 5,
"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": "rbac:tenants:read", "display_name": "查看租户列表", "module": "rbac", "resource": "tenants", "action": "read", "api_method": "GET", "api_path": "/api/v3/tenants", "route_path": "/tenants"},
{"permission_key": "rbac:tenants:create", "display_name": "创建租户", "module": "rbac", "resource": "tenants", "action": "create", "api_method": "POST", "api_path": "/api/v3/tenants", "route_path": "/tenants"},
{"permission_key": "rbac:tenants:update", "display_name": "更新租户", "module": "rbac", "resource": "tenants", "action": "update", "api_method": "PUT", "api_path": "/api/v3/tenants/{tenant_code}", "route_path": "/tenants"},
{"permission_key": "rbac:tenants:status", "display_name": "启停租户", "module": "rbac", "resource": "tenants", "action": "status", "api_method": "PATCH", "api_path": "/api/v3/tenants/{tenant_code}/status", "route_path": "/tenants"},
{"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": "contract_template:list:read", "display_name": "查看合同模板列表", "module": "contract_template", "resource": "list", "action": "read", "api_method": "GET", "api_path": "/api/v3/contract-templates", "route_path": "/contract-template/list"},
{"permission_key": "contract_template:search:read", "display_name": "搜索合同模板", "module": "contract_template", "resource": "search", "action": "read", "api_method": "GET", "api_path": "/api/v3/contract-templates/search", "route_path": "/contract-template/search"},
{"permission_key": "contract_template:detail:read", "display_name": "查看合同模板详情", "module": "contract_template", "resource": "detail", "action": "read", "api_method": "GET", "api_path": "/api/v3/contract-templates/{id}", "route_path": "/contract-template/list"},
{"permission_key": "contract_template:create:write", "display_name": "上传合同模板", "module": "contract_template", "resource": "create", "action": "write", "api_method": "POST", "api_path": "/api/v3/contract-templates", "route_path": "/contract-template/list"},
{"permission_key": "contract_template:update:write", "display_name": "更新合同模板", "module": "contract_template", "resource": "update", "action": "write", "api_method": "PUT", "api_path": "/api/v3/contract-templates/{id}", "route_path": "/contract-template/list"},
{"permission_key": "contract_template:delete:delete", "display_name": "删除合同模板", "module": "contract_template", "resource": "delete", "action": "delete", "api_method": "DELETE", "api_path": "/api/v3/contract-templates/{id}", "route_path": "/contract-template/list"},
{"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": "/rules"},
{"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": "/rules"},
{"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": "/rules"},
{"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": "/rules"},
{"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": "/rules"},
{"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:version_list:read", "display_name": "规则版本列表", "module": "rules", "resource": "version_list", "action": "read", "api_method": "GET", "api_path": "/api/rule-sets/{rule_type}/versions", "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": "rules:validate:execute", "display_name": "规则 YAML 校验", "module": "rules", "resource": "validate", "action": "execute", "api_method": "POST", "api_path": "/api/rule-sets/{rule_type}/validate", "route_path": "/rules"},
{"permission_key": "rules:version_create:write", "display_name": "创建规则版本", "module": "rules", "resource": "version_create", "action": "write", "api_method": "POST", "api_path": "/api/rule-sets/{rule_type}/versions", "route_path": "/rules"},
{"permission_key": "rules:publish:write", "display_name": "发布规则版本", "module": "rules", "resource": "publish", "action": "write", "api_method": "POST", "api_path": "/api/rule-sets/{rule_type}/publish", "route_path": "/rules"},
{"permission_key": "rules:rollback:write", "display_name": "回滚规则版本", "module": "rules", "resource": "rollback", "action": "write", "api_method": "POST", "api_path": "/api/rule-sets/{rule_type}/rollback", "route_path": "/rules"},
{"permission_key": "rules:binding_list:read", "display_name": "规则绑定列表", "module": "rules", "resource": "binding_list", "action": "read", "api_method": "GET", "api_path": "/api/rule-sets/bindings", "route_path": "/rules"},
{"permission_key": "rules:binding_create:write", "display_name": "创建规则绑定", "module": "rules", "resource": "binding_create", "action": "write", "api_method": "POST", "api_path": "/api/rule-sets/{rule_type}/bindings", "route_path": "/rules"},
{"permission_key": "rules:binding_update:write", "display_name": "更新规则绑定", "module": "rules", "resource": "binding_update", "action": "write", "api_method": "PUT", "api_path": "/api/rule-sets/bindings/{binding_id}", "route_path": "/rules"},
{"permission_key": "rules:binding_delete:delete", "display_name": "删除规则绑定", "module": "rules", "resource": "binding_delete", "action": "delete", "api_method": "DELETE", "api_path": "/api/rule-sets/bindings/{binding_id}", "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:user_tenant:update", "display_name": "更新用户租户", "module": "rbac", "resource": "user_tenant", "action": "update", "api_method": "PUT", "api_path": "/api/v3/rbac/users/{user_id}/tenant", "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"},
]
_CORE_ROLE_AUTO_GRANTS: dict[str, tuple[str, ...]] = {
"super_admin": ("rbac:user_tenant:update", "rbac:tenants:read", "rbac:tenants:create", "rbac:tenants:update", "rbac:tenants:status"),
"provincial_admin": ("rbac:user_tenant:update", "rbac:tenants:read", "rbac:tenants:create", "rbac:tenants:update", "rbac:tenants:status"),
"admin": ("rbac:user_tenant:update", "rbac:tenants:read", "rbac:tenants:create", "rbac:tenants:update", "rbac:tenants:status"),
}
async def ListRoles(self, CurrentUserId: int, Page: int, PageSize: int, RoleKey: str | None, RoleName: str | None, IncludeSystem: bool) -> RoleListVO:
"""查询角色列表。"""
await self._assertManageAndPermission(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._assertManageAndPermission(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._assertManageAndPermission(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._assertManageAndPermission(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,
TenantCode: str | None,
NickName: str | None,
) -> UserListVO:
"""查询用户列表。"""
currentUser = await self._assertManageAndPermission(CurrentUserId, "rbac:users:read")
offset = max(Page - 1, 0) * PageSize
filters = ["u.deleted_at IS NULL", "u.status = 0"]
params: dict[str, object] = {"limit": PageSize, "offset": offset}
async with GetAsyncSession() as Session:
sso_user_columns = await SsoUserCompat.get_columns(Session)
requested_scope = await self._resolve_requested_scope(Session, RawValue=TenantCode or Area)
filters, params = self._apply_user_scope_filters(
Filters=filters,
Params=params,
CurrentUser=currentUser,
RequestedScope=requested_scope,
SsoUserColumns=sso_user_columns,
alias="u",
)
if NickName:
filters.append("u.nick_name ILIKE :nick_name")
params["nick_name"] = f"%{NickName.strip()}%"
whereClause = " AND ".join(filters)
tenant_code_select = SsoUserCompat.optional_column_as(
sso_user_columns,
alias="u",
column="tenant_code",
output_alias="tenant_code",
)
tenant_name_select = SsoUserCompat.optional_column_as(
sso_user_columns,
alias="u",
column="tenant_name",
output_alias="tenant_name",
)
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, {tenant_code_select}, u.ou_name, u.ou_id, u.status,
u.is_leader, {tenant_name_select}, 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:
"""查询组织树。"""
started_at = time.perf_counter()
currentUser = await self._assertManageAndPermission(CurrentUserId, "rbac:users:read")
root_uuid = str(RootUuid or "").strip() or None
def _normalize_group_label(value: str, empty_label: str) -> str:
return "" if value == empty_label else value
def _display_group_label(value: object, empty_label: str) -> str:
text_value = str(value or "").strip()
return text_value or empty_label
async with GetAsyncSession() as Session:
sso_user_columns = await SsoUserCompat.get_columns(Session)
tenant_code_expr = SsoUserCompat.raw_optional_column(sso_user_columns, alias="sso_users", column="tenant_code")
tenant_code_expr_u = SsoUserCompat.raw_optional_column(sso_user_columns, alias="u", column="tenant_code")
tenant_name_expr = f"COALESCE({SsoUserCompat.raw_optional_column(sso_user_columns, alias='sso_users', column='tenant_name')}, area, '')"
tenant_name_expr_u = f"COALESCE({SsoUserCompat.raw_optional_column(sso_user_columns, alias='u', column='tenant_name')}, u.area, '')"
def _tenant_scope_sql(*, code_param: str, name_param: str) -> str:
return (
f"(COALESCE({tenant_code_expr}, '') = :{code_param} "
f"OR (COALESCE({tenant_code_expr}, '') = '' AND {tenant_name_expr} = :{name_param}))"
)
def _make_org_user(row: Any) -> OrganizationTreeUserVO:
tenant_name = str(row.get("tenant_name") or row.get("area") or "").strip()
dep_name = str(row.get("dep_name") or "").strip()
dep_short_name = str(row.get("dep_short_name") or "").strip()
ou_name = str(row.get("ou_name") or "").strip()
return OrganizationTreeUserVO(
id=int(row["id"]),
username=str(row.get("username") or ""),
nick_name=str(row.get("nick_name") or ""),
area=row.get("area"),
tenant_code=row.get("tenant_code"),
ou_id=str(row.get("ou_id") or ""),
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") or row.get("area"),
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,
),
)
user_filters = ["deleted_at IS NULL", "status = 0"]
params: dict[str, object] = {}
if not currentUser["is_global"]:
current_tenant_code = str(currentUser.get("tenant_code") or "").strip()
current_tenant_name = str(currentUser.get("tenant_scope_value") or "").strip()
if current_tenant_code:
user_filters.append(_tenant_scope_sql(code_param="user_tenant_code", name_param="user_tenant_name"))
params["user_tenant_code"] = current_tenant_code
params["user_tenant_name"] = current_tenant_name
else:
user_filters.append(f"{tenant_name_expr} = :user_tenant_name")
params["user_tenant_name"] = current_tenant_name
if root_uuid:
if root_uuid.startswith("tenant__"):
tenant_key = _normalize_group_label(root_uuid.removeprefix("tenant__"), self._UNGROUPED_TENANT_LABEL)
if tenant_key:
params["tenant_node_code"] = tenant_key
resolution = await self.TenantResolver.Resolve(
RawValue=tenant_key,
Source="rbac_org_tree_tenant_root",
PreferredTenantCode=tenant_key,
)
params["tenant_node_name"] = str(resolution.tenant_name or tenant_key).strip()
user_filters.append(_tenant_scope_sql(code_param="tenant_node_code", name_param="tenant_node_name"))
else:
user_filters.append(f"COALESCE({tenant_code_expr}, '') = ''")
user_filters.append(f"{tenant_name_expr} = :tenant_node_name")
params["tenant_node_name"] = ""
elif root_uuid.startswith("dep__"):
tenant_key, _, dep_label = root_uuid.removeprefix("dep__").partition("__")
tenant_key = _normalize_group_label(tenant_key, self._UNGROUPED_TENANT_LABEL)
dep_label = _normalize_group_label(dep_label, self._UNGROUPED_DEPARTMENT_LABEL)
if tenant_key:
params["tenant_node_code"] = tenant_key
resolution = await self.TenantResolver.Resolve(
RawValue=tenant_key,
Source="rbac_org_tree_department_root",
PreferredTenantCode=tenant_key,
)
params["tenant_node_name"] = str(resolution.tenant_name or tenant_key).strip()
user_filters.append(_tenant_scope_sql(code_param="tenant_node_code", name_param="tenant_node_name"))
else:
user_filters.append(f"COALESCE({tenant_code_expr}, '') = ''")
params["tenant_node_name"] = ""
user_filters.append(f"{tenant_name_expr} = :tenant_node_name")
user_filters.append("COALESCE(dep_name, dep_short_name, '') = :dep_name")
params["dep_name"] = dep_label
elif root_uuid.startswith("org__"):
tenant_key, _, remainder = root_uuid.removeprefix("org__").partition("__")
dep_label, _, org_label = remainder.partition("__")
tenant_key = _normalize_group_label(tenant_key, self._UNGROUPED_TENANT_LABEL)
dep_label = _normalize_group_label(dep_label, self._UNGROUPED_DEPARTMENT_LABEL)
org_label = _normalize_group_label(org_label, self._UNGROUPED_ORGANIZATION_LABEL)
if tenant_key:
params["tenant_node_code"] = tenant_key
resolution = await self.TenantResolver.Resolve(
RawValue=tenant_key,
Source="rbac_org_tree_org_root",
PreferredTenantCode=tenant_key,
)
params["tenant_node_name"] = str(resolution.tenant_name or tenant_key).strip()
user_filters.append(_tenant_scope_sql(code_param="tenant_node_code", name_param="tenant_node_name"))
else:
user_filters.append(f"COALESCE({tenant_code_expr}, '') = ''")
params["tenant_node_name"] = ""
user_filters.append(f"{tenant_name_expr} = :tenant_node_name")
user_filters.append("COALESCE(dep_name, dep_short_name, '') = :dep_name")
user_filters.append("COALESCE(ou_name, '') = :ou_name")
params["dep_name"] = dep_label
params["ou_name"] = org_label
else:
user_filters.append("COALESCE(ou_id, '') = :root_ou_id")
params["root_ou_id"] = root_uuid
where_clause = " AND ".join(user_filters)
total_users = int(
(
await Session.execute(
text(
f"""
SELECT COUNT(1)
FROM sso_users
WHERE {where_clause}
"""
),
params,
)
).scalar()
or 0
)
query_rows_count = 0
organizations: list[OrganizationNodeVO]
if not root_uuid:
tenant_rows = (
await Session.execute(
text(
f"""
SELECT DISTINCT
COALESCE({tenant_code_expr}, '') AS tenant_code,
{tenant_name_expr} AS tenant_name
FROM sso_users
WHERE {where_clause}
ORDER BY COALESCE({tenant_code_expr}, '') ASC, {tenant_name_expr} ASC
"""
),
params,
)
).mappings().all()
query_rows_count = len(tenant_rows)
organizations = [
OrganizationNodeVO(
ou_id=f"tenant__{_display_group_label(row.get('tenant_code'), self._UNGROUPED_TENANT_LABEL)}",
ou_name=_display_group_label(row.get("tenant_name"), self._UNGROUPED_TENANT_LABEL),
parent_ou_id=None,
level=1,
)
for row in tenant_rows
]
total_organizations = len(organizations)
elif root_uuid.startswith("tenant__"):
tenant_key = _normalize_group_label(root_uuid.removeprefix("tenant__"), self._UNGROUPED_TENANT_LABEL)
tenant_label = _display_group_label(params.get("tenant_node_name"), self._UNGROUPED_TENANT_LABEL)
dep_rows = (
await Session.execute(
text(
f"""
SELECT DISTINCT
COALESCE(dep_name, dep_short_name, '') AS department_name
FROM sso_users
WHERE {where_clause}
ORDER BY COALESCE(dep_name, dep_short_name, '') ASC
"""
),
params,
)
).mappings().all()
query_rows_count = len(dep_rows)
child_nodes = [
OrganizationNodeVO(
ou_id=f"dep__{tenant_key}__{_display_group_label(row.get('department_name'), self._UNGROUPED_DEPARTMENT_LABEL)}",
ou_name=_display_group_label(row.get("department_name"), self._UNGROUPED_DEPARTMENT_LABEL),
parent_ou_id=root_uuid,
level=2,
)
for row in dep_rows
]
organizations = [
OrganizationNodeVO(
ou_id=root_uuid,
ou_name=tenant_label,
parent_ou_id=None,
level=1,
children=child_nodes,
)
]
total_organizations = 1 + len(child_nodes)
elif root_uuid.startswith("dep__"):
tenant_key, _, dep_label = root_uuid.removeprefix("dep__").partition("__")
org_rows = (
await Session.execute(
text(
f"""
SELECT DISTINCT
COALESCE(ou_id, '') AS ou_id,
COALESCE(ou_name, '') AS ou_name
FROM sso_users
WHERE {where_clause}
ORDER BY COALESCE(ou_name, '') ASC, COALESCE(ou_id, '') ASC
"""
),
params,
)
).mappings().all()
query_rows_count = len(org_rows)
child_nodes = [
OrganizationNodeVO(
ou_id=str(row.get("ou_id") or "").strip()
or f"org__{tenant_key}__{dep_label}__{_display_group_label(row.get('ou_name'), self._UNGROUPED_ORGANIZATION_LABEL)}",
ou_name=_display_group_label(row.get("ou_name"), self._UNGROUPED_ORGANIZATION_LABEL),
parent_ou_id=root_uuid,
level=3,
)
for row in org_rows
]
organizations = [
OrganizationNodeVO(
ou_id=root_uuid,
ou_name=_display_group_label(_normalize_group_label(dep_label, self._UNGROUPED_DEPARTMENT_LABEL), self._UNGROUPED_DEPARTMENT_LABEL),
parent_ou_id=f"tenant__{tenant_key}",
level=2,
children=child_nodes,
)
]
total_organizations = 1 + len(child_nodes)
else:
user_rows = (
await Session.execute(
text(
f"""
SELECT
id,
username,
nick_name,
area,
{tenant_code_expr_u} AS tenant_code,
ou_id,
ou_name,
is_leader,
status,
{tenant_name_expr_u} AS tenant_name,
dep_name,
dep_short_name,
email,
phone_number
FROM sso_users
WHERE {where_clause}
ORDER BY is_leader DESC, COALESCE(nick_name, '') ASC, COALESCE(username, '') ASC, id ASC
"""
),
params,
)
).mappings().all()
query_rows_count = len(user_rows)
users = [_make_org_user(row) for row in user_rows] if IncludeUsers else []
total_users = len(user_rows) if IncludeUsers else total_users
if root_uuid.startswith("org__"):
tenant_key, _, remainder = root_uuid.removeprefix("org__").partition("__")
dep_label, _, org_label = remainder.partition("__")
organizations = [
OrganizationNodeVO(
ou_id=root_uuid,
ou_name=_display_group_label(_normalize_group_label(org_label, self._UNGROUPED_ORGANIZATION_LABEL), self._UNGROUPED_ORGANIZATION_LABEL),
parent_ou_id=f"dep__{tenant_key}__{dep_label}",
level=3,
users=users,
)
]
else:
first_row = user_rows[0] if user_rows else {}
tenant_key = _display_group_label(first_row.get("tenant_code"), self._UNGROUPED_TENANT_LABEL)
dep_label = _display_group_label(first_row.get("dep_name") or first_row.get("dep_short_name"), self._UNGROUPED_DEPARTMENT_LABEL)
org_label = _display_group_label(first_row.get("ou_name"), self._UNGROUPED_ORGANIZATION_LABEL)
organizations = [
OrganizationNodeVO(
ou_id=root_uuid,
ou_name=org_label,
parent_ou_id=f"dep__{tenant_key}__{dep_label}",
level=3,
users=users,
)
]
total_organizations = 1 if organizations else 0
result = OrganizationTreeVO(
organizations=organizations,
total_organizations=total_organizations,
total_users=total_users,
)
duration_ms = round((time.perf_counter() - started_at) * 1000, 2)
self._logger.info(
"rbac organization tree built: user=%s include_users=%s root=%s rows=%s orgs=%s users=%s duration_ms=%s",
CurrentUserId,
IncludeUsers,
root_uuid or "root",
query_rows_count,
result.total_organizations,
result.total_users,
duration_ms,
)
return result
async def ListRoleUsers(
self,
CurrentUserId: int,
RoleId: int,
Page: int,
PageSize: int,
Area: str | None,
TenantCode: str | None,
UserName: str | None,
) -> UserListVO:
"""查询指定角色下的用户列表。"""
currentUser = await self._assertManageAndPermission(CurrentUserId, "rbac:users:read")
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}
async with GetAsyncSession() as Session:
sso_user_columns = await SsoUserCompat.get_columns(Session)
requested_scope = await self._resolve_requested_scope(Session, RawValue=TenantCode or Area)
filters, params = self._apply_user_scope_filters(
Filters=filters,
Params=params,
CurrentUser=currentUser,
RequestedScope=requested_scope,
SsoUserColumns=sso_user_columns,
alias="u",
)
if UserName:
filters.append("(u.username ILIKE :user_name OR u.nick_name ILIKE :user_name)")
params["user_name"] = f"%{UserName.strip()}%"
whereClause = " AND ".join(filters)
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()
)
tenant_code_select = SsoUserCompat.optional_column_as(
sso_user_columns,
alias="u",
column="tenant_code",
output_alias="tenant_code",
)
tenant_name_select = SsoUserCompat.optional_column_as(
sso_user_columns,
alias="u",
column="tenant_name",
output_alias="tenant_name",
)
rows = (
await Session.execute(
text(
f"""
SELECT
u.id, u.username, u.nick_name, u.phone_number, u.email, u.area, {tenant_code_select}, u.ou_name, u.ou_id, u.status,
u.is_leader, {tenant_name_select}, 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:
"""为用户分配角色。"""
currentUser = await self._assertManageAndPermission(CurrentUserId, "rbac:user_roles:write")
await self._assertTargetUserWithinScope(CurrentUser=currentUser, UserId=UserId, Operation="分配角色")
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()
PermissionServiceImpl.InvalidateUser(UserId)
return await self.GetUserRoles(CurrentUserId, UserId)
async def UpdateUserTenant(self, CurrentUserId: int, UserId: int, Body: UserTenantUpdateDTO) -> UserTenantUpdateVO:
"""更新用户租户。"""
currentUser = await self._assertManageAndPermission(CurrentUserId, "rbac:user_tenant:update")
normalized_tenant_code = str(Body.tenant_code or "").strip()
if not normalized_tenant_code:
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "租户编码不能为空")
tenant_resolution = await self.TenantResolver.Resolve(
RawValue=None,
Source="rbac_update_user_tenant",
PreferredTenantCode=normalized_tenant_code,
)
if not tenant_resolution.tenant_code:
raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "租户不存在或未启用")
if not currentUser["is_global"] and tenant_resolution.tenant_code != currentUser.get("tenant_code"):
raise LeauditException(StatusCodeEnum.HTTP_403_FORBIDDEN, "不能修改其他租户用户")
async with GetAsyncSession() as Session:
sso_user_columns = await SsoUserCompat.get_columns(Session)
tenant_code_select = SsoUserCompat.optional_coalesce_as(
sso_user_columns,
alias="u",
column="tenant_code",
fallback_sql="''",
)
tenant_name_select = SsoUserCompat.optional_coalesce_as(
sso_user_columns,
alias="u",
column="tenant_name",
fallback_sql="''",
)
row = (
await Session.execute(
text(
f"""
SELECT
u.id,
u.username,
COALESCE(u.area, '') AS area,
{tenant_code_select},
{tenant_name_select}
FROM sso_users u
WHERE u.id = :user_id
AND u.deleted_at IS NULL
LIMIT 1
"""
),
{"user_id": UserId},
)
).mappings().first()
if not row:
raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "用户不存在")
target_user_tenant = await self.TenantResolver.ResolveUserContext(
Area=str(row.get("area") or ""),
TenantCode=str(row.get("tenant_code") or "") or None,
TenantName=str(row.get("tenant_name") or "") or None,
Source="rbac_update_user_existing_context",
)
if not currentUser["is_global"]:
existing_target_tenant_code = target_user_tenant.tenant_code or str(row.get("tenant_code") or "") or None
if existing_target_tenant_code and existing_target_tenant_code != currentUser.get("tenant_code"):
raise LeauditException(StatusCodeEnum.HTTP_403_FORBIDDEN, "不能修改其他租户用户")
update_payload = {
"user_id": UserId,
"tenant_code": tenant_resolution.tenant_code,
"tenant_name": tenant_resolution.tenant_name or "",
"area": tenant_resolution.tenant_name or "",
}
await Session.execute(
text(
"""
UPDATE sso_users
SET
tenant_code = :tenant_code,
tenant_name = :tenant_name,
area = :area,
updated_at = NOW()
WHERE id = :user_id
AND deleted_at IS NULL
"""
),
update_payload,
)
await Session.commit()
PermissionServiceImpl.InvalidateUser(UserId)
return UserTenantUpdateVO(
user_id=UserId,
username=str(row.get("username") or ""),
area=tenant_resolution.tenant_name or "",
tenant_code=tenant_resolution.tenant_code,
tenant_name=tenant_resolution.tenant_name,
)
async def RevokeUserRole(self, CurrentUserId: int, UserId: int, RoleId: int) -> None:
"""移除用户角色。"""
currentUser = await self._assertManageAndPermission(CurrentUserId, "rbac:user_roles:write")
await self._assertTargetUserWithinScope(CurrentUser=currentUser, UserId=UserId, Operation="移除角色")
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()
PermissionServiceImpl.InvalidateUser(UserId)
async def GetUserRoles(self, CurrentUserId: int, UserId: int) -> UserRolesVO:
"""查询用户角色。"""
currentUser = await self._assertManageAndPermission(CurrentUserId, "rbac:users:read")
await self._assertTargetUserWithinScope(CurrentUser=currentUser, UserId=UserId, Operation="查询角色")
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._getAdminRouteMap(Session, EnsureSeeds=False)
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._assertManageAndPermission(CurrentUserId, "rbac:roles:read")
async with GetAsyncSession() as Session:
routeMap = await self._getAdminRouteMap(Session, EnsureSeeds=False)
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._assertManageAndPermission(CurrentUserId, "rbac:role_routes:write")
routeIds = sorted(set(Body.route_ids))
async with GetAsyncSession() as Session:
await self._getAdminRouteMap(Session, EnsureSeeds=True)
result = await self._updateRoleRoutesInSession(Session, RoleId, routeIds, Body.permission)
await Session.commit()
PermissionServiceImpl.InvalidateAll()
return result
async def GetRolePermissions(self, CurrentUserId: int, RoleId: int) -> RolePermissionsVO:
"""查询角色权限授权。"""
await self._assertManageAndPermission(CurrentUserId, "rbac:roles:read")
async with GetAsyncSession() as Session:
await self._getAdminRouteMap(Session, EnsureSeeds=False)
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._assertManageAndPermission(CurrentUserId, "rbac:role_permissions:write")
async with GetAsyncSession() as Session:
await self._getAdminRouteMap(Session, EnsureSeeds=True)
await self._saveRolePermissionsInSession(Session, Body.role_id, Body.permissions, Body.replace, Body.replace_scope_permission_ids)
await Session.commit()
PermissionServiceImpl.InvalidateAll()
return await self.GetRolePermissions(CurrentUserId, Body.role_id)
async def SaveRoleAccess(self, CurrentUserId: int, RoleId: int, Body: RoleAccessSaveDTO) -> RoleAccessSaveVO:
"""原子保存角色菜单与接口权限。"""
await self._assertManageAndPermissions(CurrentUserId, ["rbac:role_routes:write", "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._getAdminRouteMap(Session, EnsureSeeds=True)
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()
PermissionServiceImpl.InvalidateAll()
return RoleAccessSaveVO(role_id=RoleId, route_result=routeResult, permission_result=permissionResult)
async def GetRoutePermissions(self, CurrentUserId: int, RouteId: int) -> RoutePermissionsVO:
"""查询路由关联权限定义。"""
await self._assertManageAndPermission(CurrentUserId, "rbac:permissions:read")
async with GetAsyncSession() as Session:
await self._getAdminRouteMap(Session, EnsureSeeds=False)
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])
def _get_cached_admin_seed_route_map(self) -> dict[str, int] | None:
"""返回近期已确认的 RBAC 管理路由映射,避免高频读接口反复 upsert。"""
if not self._admin_seed_route_map_cache:
return None
if time.monotonic() - self._admin_seed_route_map_cached_at > self._ADMIN_SEED_CACHE_TTL_SECONDS:
return None
expectedPaths = {str(item["route_path"]) for item in self._MANAGEABLE_ROUTE_BLUEPRINTS}
if set(self._admin_seed_route_map_cache.keys()) != expectedPaths:
self.__class__._admin_seed_route_map_cache = None
self.__class__._admin_seed_route_map_cached_at = 0.0
return None
return dict(self._admin_seed_route_map_cache)
def _remember_admin_seed_route_map(self, RouteMap: dict[str, int]) -> None:
self.__class__._admin_seed_route_map_cache = dict(RouteMap)
self.__class__._admin_seed_route_map_cached_at = time.monotonic()
async def _getAdminRouteMap(self, Session, *, EnsureSeeds: bool) -> dict[str, int]:
"""读取可管理路由映射;写接口或缓存失效时才执行 seed。"""
cached = self._get_cached_admin_seed_route_map()
if cached:
return cached
if EnsureSeeds:
return await self._ensureAdminSeeds(Session)
paths = [str(item["route_path"]) for item in self._MANAGEABLE_ROUTE_BLUEPRINTS]
rows = (
await Session.execute(
text(
"""
SELECT route_path, id
FROM sys_routes
WHERE deleted_at IS NULL
AND route_path = ANY(:paths)
"""
).bindparams(paths=paths)
)
).mappings().all()
routeMap = {str(row["route_path"]): int(row["id"]) for row in rows}
if len(routeMap) == len(paths):
return await self._ensureAdminSeeds(Session)
return await self._ensureAdminSeeds(Session)
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 自动放行。"""
await self._assertPermissions(CurrentUserId, [PermissionKey])
async def _assertPermissions(self, CurrentUserId: int, PermissionKeys: list[str]) -> dict[str, Any]:
"""一次性校验管理能力和多个细粒度权限,返回当前用户上下文。"""
context = await self._getCurrentUserContext(CurrentUserId)
if not context["can_manage"]:
raise LeauditException(StatusCodeEnum.HTTP_403_FORBIDDEN, "当前用户没有系统设置管理权限")
permissionKeys = sorted({str(key).strip() for key in PermissionKeys if str(key).strip()})
if context["is_super_admin"] or not permissionKeys:
return context
async with GetAsyncSession() as Session:
grantedRows = (
await Session.execute(
text(
"""
SELECT DISTINCT p.permission_key
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 = ANY(:permission_keys)
AND rp.grant_type = 'GRANT'
"""
).bindparams(permission_keys=permissionKeys),
{"user_id": CurrentUserId},
)
).mappings().all()
granted = {str(row["permission_key"] or "") for row in grantedRows}
missing = [key for key in permissionKeys if key not in granted]
if not missing:
return context
displayRows = (
await Session.execute(
text(
"""
SELECT permission_key, display_name
FROM permissions
WHERE permission_key = ANY(:permission_keys)
"""
).bindparams(permission_keys=missing)
)
).mappings().all()
displayByKey = {str(row["permission_key"] or ""): str(row["display_name"] or "") for row in displayRows}
displayName = displayByKey.get(missing[0]) or missing[0]
raise LeauditException(StatusCodeEnum.HTTP_403_FORBIDDEN, f"缺少「{displayName}」权限")
async def _assertManageAndPermission(self, CurrentUserId: int, PermissionKey: str) -> dict[str, Any]:
"""兼容单权限场景的合并校验。"""
return await self._assertPermissions(CurrentUserId, [PermissionKey])
async def _assertManageAndPermissions(self, CurrentUserId: int, PermissionKeys: list[str]) -> dict[str, Any]:
"""兼容多权限场景的合并校验。"""
return await self._assertPermissions(CurrentUserId, PermissionKeys)
async def _assertPermissionLegacy(self, CurrentUserId: int, PermissionKey: str) -> None:
"""保留旧逻辑参考;不再由热路径调用。"""
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:
sso_user_columns = await SsoUserCompat.get_columns(Session)
tenant_code_select = SsoUserCompat.optional_coalesce_as(
sso_user_columns,
alias="u",
column="tenant_code",
fallback_sql="''",
)
tenant_name_select = SsoUserCompat.optional_coalesce_as(
sso_user_columns,
alias="u",
column="tenant_name",
fallback_sql="''",
)
row = (
await Session.execute(
text(
f"""
SELECT
u.id,
COALESCE(u.area, '') AS area,
{tenant_code_select},
{tenant_name_select},
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, "当前用户不存在")
tenant = await self.TenantResolver.ResolveUserContext(
Area=str(row["area"] or ""),
TenantCode=str(row["tenant_code"] or "") or None,
TenantName=str(row["tenant_name"] or "") or None,
Source="rbac_admin_user_context",
)
return {
"area": str(row["area"] or ""),
"tenant_code": tenant.tenant_code or str(row["tenant_code"] or "") or None,
"tenant_name": tenant.tenant_name or str(row["tenant_name"] or "") or str(row["area"] or "") or None,
"tenant_scope_value": tenant.tenant_name or tenant.normalized_value or 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 _resolve_requested_scope(self, Session, RawValue: str | None) -> dict[str, str]:
"""把显式 area 参数统一解析为租户编码 + 展示值。"""
normalized = str(RawValue or "").strip()
if not normalized:
return {"tenant_code": "", "tenant_name": "", "normalized_value": ""}
resolution = await self.TenantResolver.Resolve(
RawValue=normalized,
Source="rbac_admin_query_scope",
PreferredTenantCode=normalized,
)
return {
"tenant_code": str(resolution.tenant_code or "").strip(),
"tenant_name": str(resolution.tenant_name or "").strip(),
"normalized_value": str(resolution.normalized_value or normalized).strip(),
}
def _apply_user_scope_filters(
self,
*,
Filters: list[str],
Params: dict[str, object],
CurrentUser: dict[str, Any],
RequestedScope: dict[str, str],
SsoUserColumns: set[str],
alias: str,
) -> tuple[list[str], dict[str, object]]:
tenant_code_expr = SsoUserCompat.raw_optional_column(SsoUserColumns, alias=alias, column="tenant_code")
tenant_name_expr = f"COALESCE({SsoUserCompat.raw_optional_column(SsoUserColumns, alias=alias, column='tenant_name')}, {alias}.area, '')"
requested_tenant_code = str(RequestedScope.get("tenant_code") or "").strip()
requested_tenant_name = str(RequestedScope.get("tenant_name") or RequestedScope.get("normalized_value") or "").strip()
current_tenant_code = str(CurrentUser.get("tenant_code") or "").strip()
current_tenant_name = str(CurrentUser.get("tenant_scope_value") or "").strip()
if requested_tenant_code or requested_tenant_name:
if not CurrentUser["is_global"]:
if requested_tenant_code:
if not current_tenant_code or requested_tenant_code != current_tenant_code:
raise LeauditException(StatusCodeEnum.HTTP_403_FORBIDDEN, "不能查询其他租户用户")
elif requested_tenant_name != current_tenant_name:
raise LeauditException(StatusCodeEnum.HTTP_403_FORBIDDEN, "不能查询其他租户用户")
if requested_tenant_code:
Filters.append(f"COALESCE({tenant_code_expr}, '') = :query_tenant_code")
Params["query_tenant_code"] = requested_tenant_code
else:
Filters.append(f"{tenant_name_expr} = :query_tenant_name")
Params["query_tenant_name"] = requested_tenant_name
elif not CurrentUser["is_global"]:
if current_tenant_code:
Filters.append(f"COALESCE({tenant_code_expr}, '') = :user_tenant_code")
Params["user_tenant_code"] = current_tenant_code
else:
Filters.append(f"{tenant_name_expr} = :user_tenant_name")
Params["user_tenant_name"] = current_tenant_name
return Filters, Params
async def _assertTargetUserWithinScope(self, *, CurrentUser: dict[str, Any], UserId: int, Operation: str) -> None:
"""校验目标用户是否处于当前管理员可操作租户范围。"""
if CurrentUser["is_global"]:
return
async with GetAsyncSession() as Session:
sso_user_columns = await SsoUserCompat.get_columns(Session)
tenant_code_select = SsoUserCompat.optional_coalesce_as(
sso_user_columns,
alias="u",
column="tenant_code",
fallback_sql="''",
)
tenant_name_select = SsoUserCompat.optional_coalesce_as(
sso_user_columns,
alias="u",
column="tenant_name",
fallback_sql="''",
)
row = (
await Session.execute(
text(
f"""
SELECT
u.id,
COALESCE(u.area, '') AS area,
{tenant_code_select},
{tenant_name_select}
FROM sso_users u
WHERE u.id = :user_id
AND u.deleted_at IS NULL
LIMIT 1
"""
),
{"user_id": UserId},
)
).mappings().first()
if not row:
raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "用户不存在")
target_tenant = await self.TenantResolver.ResolveUserContext(
Area=str(row.get("area") or ""),
TenantCode=str(row.get("tenant_code") or "") or None,
TenantName=str(row.get("tenant_name") or "") or None,
Source="rbac_target_user_scope_check",
)
target_tenant_code = str(target_tenant.tenant_code or row.get("tenant_code") or "").strip()
target_tenant_name = str(target_tenant.tenant_name or row.get("tenant_name") or row.get("area") or "").strip()
current_tenant_code = str(CurrentUser.get("tenant_code") or "").strip()
current_tenant_name = str(CurrentUser.get("tenant_scope_value") or "").strip()
if current_tenant_code:
if target_tenant_code and target_tenant_code != current_tenant_code:
raise LeauditException(StatusCodeEnum.HTTP_403_FORBIDDEN, f"不能{Operation}其他租户用户")
if not target_tenant_code and target_tenant_name and target_tenant_name != current_tenant_name:
raise LeauditException(StatusCodeEnum.HTTP_403_FORBIDDEN, f"不能{Operation}其他租户用户")
return
if target_tenant_name != current_tenant_name:
raise LeauditException(StatusCodeEnum.HTTP_403_FORBIDDEN, f"不能{Operation}其他租户用户")
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"],
},
)
for role_key, permission_keys in self._CORE_ROLE_AUTO_GRANTS.items():
if not permission_keys:
continue
await Session.execute(
text(
"""
INSERT INTO role_permissions (role_id, permission_id, grant_type, data_scope, created_at, updated_at)
SELECT
r.id,
p.id,
'GRANT',
'ALL',
NOW(),
NOW()
FROM roles r
JOIN permissions p
ON p.permission_key = ANY(:permission_keys)
WHERE r.role_key = :role_key
AND NOT EXISTS (
SELECT 1
FROM role_permissions rp
WHERE rp.role_id = r.id
AND rp.permission_id = p.id
)
"""
).bindparams(permission_keys=list(permission_keys)),
{"role_key": role_key},
)
for role_key, route_paths in self._CORE_ROLE_AUTO_ROUTE_PATHS.items():
if not route_paths:
continue
await Session.execute(
text(
"""
INSERT INTO role_route (role_id, route_id, permission, status, created_at, updated_at)
SELECT
r.id,
sr.id,
'RW',
1,
NOW(),
NOW()
FROM roles r
JOIN sys_routes sr ON sr.route_path = ANY(:route_paths)
WHERE r.role_key = :role_key
AND sr.deleted_at IS NULL
AND NOT EXISTS (
SELECT 1
FROM role_route rr
WHERE rr.role_id = r.id
AND rr.route_id = sr.id
)
"""
).bindparams(route_paths=list(route_paths)),
{"role_key": role_key},
)
await Session.commit()
self._remember_admin_seed_route_map(routeIdsByPath)
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"),
tenant_code=Row.get("tenant_code"),
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)