From e19f63183b09e17d3936464e9fc704ac485a1388 Mon Sep 17 00:00:00 2001 From: wren <“porlong@qq.com”> Date: Mon, 11 May 2026 09:38:14 +0800 Subject: [PATCH] feat(rbac): add lazy organization tree endpoint --- .../controllers/rbacAdminController.py | 10 + .../services/impl/rbacAdminServiceImpl.py | 178 +++++++++++++++++- 2 files changed, 186 insertions(+), 2 deletions(-) diff --git a/fastapi_modules/fastapi_leaudit/controllers/rbacAdminController.py b/fastapi_modules/fastapi_leaudit/controllers/rbacAdminController.py index a6113b6..7669ebf 100644 --- a/fastapi_modules/fastapi_leaudit/controllers/rbacAdminController.py +++ b/fastapi_modules/fastapi_leaudit/controllers/rbacAdminController.py @@ -63,6 +63,16 @@ class RbacAdminController(BaseController): data = await self.RbacAdminService.ListUsers(int(payload["user_id"]), page, page_size, area, nick_name) return JSONResponse(status_code=200, content={"code": 200, "message": "success", "data": data.model_dump()}) + @self.router.get("/admin/users/organizations/tree") + async def GetOrganizationTree( + payload: dict[str, Any] = Depends(verify_access_token), + include_users: bool = Query(False), + root_uuid: str | None = Query(None), + ): + """查询组织树。""" + data = await self.RbacAdminService.GetOrganizationTree(int(payload["user_id"]), include_users, root_uuid) + return JSONResponse(status_code=200, content={"code": 200, "message": "success", "data": data.model_dump()}) + @self.router.get("/v3/rbac/roles/{RoleId}/users") async def GetRoleUsers( RoleId: int, diff --git a/fastapi_modules/fastapi_leaudit/services/impl/rbacAdminServiceImpl.py b/fastapi_modules/fastapi_leaudit/services/impl/rbacAdminServiceImpl.py index bee20f9..7a9cdcd 100644 --- a/fastapi_modules/fastapi_leaudit/services/impl/rbacAdminServiceImpl.py +++ b/fastapi_modules/fastapi_leaudit/services/impl/rbacAdminServiceImpl.py @@ -19,6 +19,10 @@ from fastapi_modules.fastapi_leaudit.domian.Dto.rbacAdminDto import ( RoleUpdateDTO, ) from fastapi_modules.fastapi_leaudit.domian.vo.rbacAdminVo import ( + OrganizationNodeVO, + OrganizationPathVO, + OrganizationTreeUserVO, + OrganizationTreeVO, RoleAccessSaveVO, RoleListVO, RolePermissionsVO, @@ -390,7 +394,7 @@ class RbacAdminServiceImpl(IRbacAdminService): 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.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) @@ -411,6 +415,175 @@ class RbacAdminServiceImpl(IRbacAdminService): ).mappings().all() return UserListVO(total=total, page=Page, page_size=PageSize, items=[self._toUserVo(row) for row in rows]) + async def GetOrganizationTree(self, CurrentUserId: int, IncludeUsers: bool, RootUuid: str | None) -> OrganizationTreeVO: + """查询组织树。""" + await self._assertManagePermission(CurrentUserId) + await self._assertPermission(CurrentUserId, "rbac:users:read") + currentUser = await self._getCurrentUserContext(CurrentUserId) + + user_filters = ["deleted_at IS NULL", "status = 0"] + params: dict[str, object] = {} + if not currentUser["is_global"]: + user_filters.append("COALESCE(area, '') = :user_area") + params["user_area"] = currentUser["area"] + where_clause = " AND ".join(user_filters) + + async with GetAsyncSession() as Session: + user_rows = ( + await Session.execute( + text( + f""" + SELECT + id, + username, + nick_name, + area, + ou_id, + ou_name, + is_leader, + status, + tenant_name, + dep_name, + dep_short_name, + email, + phone_number + FROM sso_users + WHERE {where_clause} + ORDER BY COALESCE(tenant_name, '') ASC, COALESCE(dep_name, '') ASC, COALESCE(ou_name, '') ASC, id ASC + """ + ), + params, + ) + ).mappings().all() + + root_uuid = str(RootUuid or "").strip() or None + tenant_nodes: dict[str, OrganizationNodeVO] = {} + department_nodes: dict[str, OrganizationNodeVO] = {} + organization_nodes: dict[str, OrganizationNodeVO] = {} + department_children: dict[str, list[str]] = {} + organization_children: dict[str, list[str]] = {} + users_by_org: dict[str, list[OrganizationTreeUserVO]] = {} + total_users = len(user_rows) + + for row in user_rows: + ou_id = str(row.get("ou_id") or "").strip() + ou_name = str(row.get("ou_name") or "").strip() or ou_id + tenant_name = str(row.get("tenant_name") or "").strip() + dep_name = str(row.get("dep_name") or "").strip() + dep_short_name = str(row.get("dep_short_name") or "").strip() + tenant_label = tenant_name or "未分组租户" + department_label = dep_name or dep_short_name or "未分组部门" + organization_label = ou_name or "未分组组织" + + tenant_key = f"tenant__{tenant_label}" + dep_key = f"dep__{tenant_label}__{department_label}" + org_key = ou_id or f"org__{tenant_label}__{department_label}__{organization_label}" + + if tenant_key not in tenant_nodes: + tenant_nodes[tenant_key] = OrganizationNodeVO( + ou_id=tenant_key, + ou_name=tenant_label, + parent_ou_id=None, + level=1, + ) + if dep_key not in department_nodes: + department_nodes[dep_key] = OrganizationNodeVO( + ou_id=dep_key, + ou_name=department_label, + parent_ou_id=tenant_key, + level=2, + ) + department_children.setdefault(tenant_key, []).append(dep_key) + if org_key not in organization_nodes: + organization_nodes[org_key] = OrganizationNodeVO( + ou_id=org_key, + ou_name=organization_label, + parent_ou_id=dep_key, + level=3, + ) + organization_children.setdefault(dep_key, []).append(org_key) + + if IncludeUsers and ou_id: + users_by_org.setdefault(org_key, []).append( + OrganizationTreeUserVO( + id=int(row["id"]), + username=str(row.get("username") or ""), + nick_name=str(row.get("nick_name") or ""), + area=row.get("area"), + ou_id=ou_id, + ou_name=ou_name, + is_leader=bool(row.get("is_leader", False)), + status=int(row.get("status") or 0), + tenant_name=row.get("tenant_name"), + dep_name=row.get("dep_name"), + dep_short_name=row.get("dep_short_name"), + email=row.get("email"), + phone_number=row.get("phone_number"), + organization_path=OrganizationPathVO( + tenant_name=tenant_name, + dep_name=dep_name, + dep_short_name=dep_short_name, + ou_name=ou_name, + ), + ) + ) + + for org_key, users in users_by_org.items(): + users.sort(key=lambda item: (not item.is_leader, item.nick_name, item.username, item.id)) + node = organization_nodes.get(org_key) + if node: + node.users = users + + def clone_node(source: OrganizationNodeVO, *, children: list[OrganizationNodeVO] | None = None, users: list[OrganizationTreeUserVO] | None = None) -> OrganizationNodeVO: + return OrganizationNodeVO( + ou_id=source.ou_id, + ou_name=source.ou_name, + parent_ou_id=source.parent_ou_id, + level=source.level, + children=children or [], + users=users or [], + ) + + if root_uuid: + if root_uuid in tenant_nodes: + tenant = tenant_nodes[root_uuid] + child_nodes = [ + clone_node(department_nodes[dep_id]) + for dep_id in sorted(set(department_children.get(root_uuid, [])), key=lambda item: (department_nodes[item].ou_name, item)) + ] + organizations = [clone_node(tenant, children=child_nodes)] + total_organizations = 1 + len(child_nodes) + elif root_uuid in department_nodes: + department = department_nodes[root_uuid] + child_nodes = [ + clone_node(organization_nodes[org_id]) + for org_id in sorted(set(organization_children.get(root_uuid, [])), key=lambda item: (organization_nodes[item].ou_name, item)) + ] + organizations = [clone_node(department, children=child_nodes)] + total_organizations = 1 + len(child_nodes) + elif root_uuid in organization_nodes: + organization = organization_nodes[root_uuid] + organization_users = list(users_by_org.get(root_uuid, [])) if IncludeUsers else [] + organizations = [clone_node(organization, users=organization_users)] + total_organizations = 1 + total_users = len(organization_users) if IncludeUsers else total_users + else: + organizations = [] + total_organizations = 0 + total_users = 0 + else: + organizations = [ + clone_node(tenant_nodes[tenant_id]) + for tenant_id in sorted(tenant_nodes.keys(), key=lambda item: (tenant_nodes[item].ou_name, item)) + ] + total_organizations = len(organizations) + + return OrganizationTreeVO( + organizations=organizations, + total_organizations=total_organizations, + total_users=total_users, + ) + async def ListRoleUsers(self, CurrentUserId: int, RoleId: int, Page: int, PageSize: int, Area: str | None, UserName: str | None) -> UserListVO: """查询指定角色下的用户列表。""" await self._assertManagePermission(CurrentUserId) @@ -452,7 +625,7 @@ class RbacAdminServiceImpl(IRbacAdminService): 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.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) @@ -976,6 +1149,7 @@ class RbacAdminServiceImpl(IRbacAdminService): 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: