Optimize RBAC org tree loading

This commit is contained in:
wren
2026-05-12 16:53:22 +08:00
parent cd21a82168
commit d47f499e57
6 changed files with 311 additions and 145 deletions
@@ -2,6 +2,8 @@
from __future__ import annotations
import logging
import time
from datetime import datetime
from typing import Any
@@ -43,6 +45,11 @@ from fastapi_modules.fastapi_leaudit.services.rbacAdminService import IRbacAdmin
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",
@@ -421,172 +428,277 @@ class RbacAdminServiceImpl(IRbacAdminService):
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:
user_rows = (
await Session.execute(
text(
f"""
SELECT
id,
username,
nick_name,
area,
ou_id,
ou_name,
is_leader,
status,
tenant_name,
dep_name,
dep_short_name,
email,
phone_number
FROM sso_users
WHERE {where_clause}
ORDER BY COALESCE(tenant_name, '') ASC, COALESCE(dep_name, '') ASC, COALESCE(ou_name, '') ASC, id ASC
"""
),
params,
)
).mappings().all()
root_uuid = str(RootUuid or "").strip() or None
tenant_nodes: dict[str, OrganizationNodeVO] = {}
department_nodes: dict[str, OrganizationNodeVO] = {}
organization_nodes: dict[str, OrganizationNodeVO] = {}
department_children: dict[str, list[str]] = {}
organization_children: dict[str, list[str]] = {}
users_by_org: dict[str, list[OrganizationTreeUserVO]] = {}
total_users = len(user_rows)
for row in user_rows:
ou_id = str(row.get("ou_id") or "").strip()
ou_name = str(row.get("ou_name") or "").strip() or ou_id
tenant_name = str(row.get("tenant_name") or "").strip()
dep_name = str(row.get("dep_name") or "").strip()
dep_short_name = str(row.get("dep_short_name") or "").strip()
tenant_label = tenant_name or "未分组租户"
department_label = dep_name or dep_short_name or "未分组部门"
organization_label = ou_name or "未分组组织"
tenant_key = f"tenant__{tenant_label}"
dep_key = f"dep__{tenant_label}__{department_label}"
org_key = ou_id or f"org__{tenant_label}__{department_label}__{organization_label}"
if tenant_key not in tenant_nodes:
tenant_nodes[tenant_key] = OrganizationNodeVO(
ou_id=tenant_key,
ou_name=tenant_label,
parent_ou_id=None,
level=1,
)
if dep_key not in department_nodes:
department_nodes[dep_key] = OrganizationNodeVO(
ou_id=dep_key,
ou_name=department_label,
parent_ou_id=tenant_key,
level=2,
)
department_children.setdefault(tenant_key, []).append(dep_key)
if org_key not in organization_nodes:
organization_nodes[org_key] = OrganizationNodeVO(
ou_id=org_key,
ou_name=organization_label,
parent_ou_id=dep_key,
level=3,
)
organization_children.setdefault(dep_key, []).append(org_key)
if IncludeUsers and ou_id:
users_by_org.setdefault(org_key, []).append(
OrganizationTreeUserVO(
id=int(row["id"]),
username=str(row.get("username") or ""),
nick_name=str(row.get("nick_name") or ""),
area=row.get("area"),
ou_id=ou_id,
ou_name=ou_name,
is_leader=bool(row.get("is_leader", False)),
status=int(row.get("status") or 0),
tenant_name=row.get("tenant_name"),
dep_name=row.get("dep_name"),
dep_short_name=row.get("dep_short_name"),
email=row.get("email"),
phone_number=row.get("phone_number"),
organization_path=OrganizationPathVO(
tenant_name=tenant_name,
dep_name=dep_name,
dep_short_name=dep_short_name,
ou_name=ou_name,
total_users = int(
(
await Session.execute(
text(
f"""
SELECT COUNT(1)
FROM sso_users
WHERE {where_clause}
"""
),
params,
)
)
for org_key, users in users_by_org.items():
users.sort(key=lambda item: (not item.is_leader, item.nick_name, item.username, item.id))
node = organization_nodes.get(org_key)
if node:
node.users = users
def clone_node(source: OrganizationNodeVO, *, children: list[OrganizationNodeVO] | None = None, users: list[OrganizationTreeUserVO] | None = None) -> OrganizationNodeVO:
return OrganizationNodeVO(
ou_id=source.ou_id,
ou_name=source.ou_name,
parent_ou_id=source.parent_ou_id,
level=source.level,
children=children or [],
users=users or [],
).scalar()
or 0
)
if root_uuid:
if root_uuid in tenant_nodes:
tenant = tenant_nodes[root_uuid]
child_nodes = [
clone_node(department_nodes[dep_id])
for dep_id in sorted(set(department_children.get(root_uuid, [])), key=lambda item: (department_nodes[item].ou_name, item))
]
organizations = [clone_node(tenant, children=child_nodes)]
total_organizations = 1 + len(child_nodes)
elif root_uuid in department_nodes:
department = department_nodes[root_uuid]
child_nodes = [
clone_node(organization_nodes[org_id])
for org_id in sorted(set(organization_children.get(root_uuid, [])), key=lambda item: (organization_nodes[item].ou_name, item))
]
organizations = [clone_node(department, children=child_nodes)]
total_organizations = 1 + len(child_nodes)
elif root_uuid in organization_nodes:
organization = organization_nodes[root_uuid]
organization_users = list(users_by_org.get(root_uuid, [])) if IncludeUsers else []
organizations = [clone_node(organization, users=organization_users)]
total_organizations = 1
total_users = len(organization_users) if IncludeUsers else total_users
else:
organizations = []
total_organizations = 0
total_users = 0
else:
organizations = [
clone_node(tenant_nodes[tenant_id])
for tenant_id in sorted(tenant_nodes.keys(), key=lambda item: (tenant_nodes[item].ou_name, item))
]
total_organizations = len(organizations)
query_rows_count = 0
organizations: list[OrganizationNodeVO]
return OrganizationTreeVO(
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:
"""查询指定角色下的用户列表。"""