"""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,
)
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 管理服务实现。"""

    _logger = logging.getLogger("APP")
    _UNGROUPED_TENANT_LABEL = "未分组租户"
    _UNGROUPED_DEPARTMENT_LABEL = "未分组部门"
    _UNGROUPED_ORGANIZATION_LABEL = "未分组组织"

    _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:
        """查询组织树。"""
        started_at = time.perf_counter()
        await self._assertManagePermission(CurrentUserId)
        await self._assertPermission(CurrentUserId, "rbac:users:read")
        currentUser = await self._getCurrentUserContext(CurrentUserId)
        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

        def _make_org_user(row: Any) -> OrganizationTreeUserVO:
            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()
            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"),
                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"),
                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"]:
            user_filters.append("COALESCE(area, '') = :user_area")
            params["user_area"] = currentUser["area"]

        if root_uuid:
            if root_uuid.startswith("tenant__"):
                tenant_label = _normalize_group_label(root_uuid.removeprefix("tenant__"), self._UNGROUPED_TENANT_LABEL)
                user_filters.append("COALESCE(tenant_name, '') = :tenant_name")
                params["tenant_name"] = tenant_label
            elif root_uuid.startswith("dep__"):
                tenant_label, _, dep_label = root_uuid.removeprefix("dep__").partition("__")
                tenant_label = _normalize_group_label(tenant_label, self._UNGROUPED_TENANT_LABEL)
                dep_label = _normalize_group_label(dep_label, self._UNGROUPED_DEPARTMENT_LABEL)
                user_filters.append("COALESCE(tenant_name, '') = :tenant_name")
                user_filters.append("COALESCE(dep_name, dep_short_name, '') = :dep_name")
                params["tenant_name"] = tenant_label
                params["dep_name"] = dep_label
            elif root_uuid.startswith("org__"):
                tenant_label, _, remainder = root_uuid.removeprefix("org__").partition("__")
                dep_label, _, org_label = remainder.partition("__")
                tenant_label = _normalize_group_label(tenant_label, 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)
                user_filters.append("COALESCE(tenant_name, '') = :tenant_name")
                user_filters.append("COALESCE(dep_name, dep_short_name, '') = :dep_name")
                user_filters.append("COALESCE(ou_name, '') = :ou_name")
                params["tenant_name"] = tenant_label
                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)

        async with GetAsyncSession() as Session:
            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_name, '') AS tenant_name
                            FROM sso_users
                            WHERE {where_clause}
                            ORDER BY COALESCE(tenant_name, '') ASC
                            """
                        ),
                        params,
                    )
                ).mappings().all()
                query_rows_count = len(tenant_rows)
                organizations = [
                    OrganizationNodeVO(
                        ou_id=f"tenant__{_display_group_label(row.get('tenant_name'), 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_label = _display_group_label(params.get("tenant_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_label}__{_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_label, _, 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_label}__{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_label}",
                        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,
                                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 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_label, _, 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_label}__{dep_label}",
                            level=3,
                            users=users,
                        )
                    ]
                else:
                    first_row = user_rows[0] if user_rows else {}
                    tenant_label = _display_group_label(first_row.get("tenant_name"), 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_label}__{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, 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)
