feat(rbac): add lazy organization tree endpoint
This commit is contained in:
@@ -63,6 +63,16 @@ class RbacAdminController(BaseController):
|
|||||||
data = await self.RbacAdminService.ListUsers(int(payload["user_id"]), page, page_size, area, nick_name)
|
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()})
|
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")
|
@self.router.get("/v3/rbac/roles/{RoleId}/users")
|
||||||
async def GetRoleUsers(
|
async def GetRoleUsers(
|
||||||
RoleId: int,
|
RoleId: int,
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ from fastapi_modules.fastapi_leaudit.domian.Dto.rbacAdminDto import (
|
|||||||
RoleUpdateDTO,
|
RoleUpdateDTO,
|
||||||
)
|
)
|
||||||
from fastapi_modules.fastapi_leaudit.domian.vo.rbacAdminVo import (
|
from fastapi_modules.fastapi_leaudit.domian.vo.rbacAdminVo import (
|
||||||
|
OrganizationNodeVO,
|
||||||
|
OrganizationPathVO,
|
||||||
|
OrganizationTreeUserVO,
|
||||||
|
OrganizationTreeVO,
|
||||||
RoleAccessSaveVO,
|
RoleAccessSaveVO,
|
||||||
RoleListVO,
|
RoleListVO,
|
||||||
RolePermissionsVO,
|
RolePermissionsVO,
|
||||||
@@ -390,7 +394,7 @@ class RbacAdminServiceImpl(IRbacAdminService):
|
|||||||
f"""
|
f"""
|
||||||
SELECT
|
SELECT
|
||||||
u.id, u.username, u.nick_name, u.phone_number, u.email, u.area, u.ou_name, u.ou_id, u.status,
|
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(
|
COALESCE(
|
||||||
json_agg(
|
json_agg(
|
||||||
DISTINCT jsonb_build_object('role_id', r.id, 'role_key', r.role_key, 'role_name', r.role_name)
|
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()
|
).mappings().all()
|
||||||
return UserListVO(total=total, page=Page, page_size=PageSize, items=[self._toUserVo(row) for row in rows])
|
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:
|
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._assertManagePermission(CurrentUserId)
|
||||||
@@ -452,7 +625,7 @@ class RbacAdminServiceImpl(IRbacAdminService):
|
|||||||
f"""
|
f"""
|
||||||
SELECT
|
SELECT
|
||||||
u.id, u.username, u.nick_name, u.phone_number, u.email, u.area, u.ou_name, u.ou_id, u.status,
|
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(
|
COALESCE(
|
||||||
json_agg(
|
json_agg(
|
||||||
DISTINCT jsonb_build_object('role_id', r.id, 'role_key', r.role_key, 'role_name', r.role_name)
|
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,
|
roles=roles,
|
||||||
tenant_name=Row.get("tenant_name"),
|
tenant_name=Row.get("tenant_name"),
|
||||||
dep_name=Row.get("dep_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:
|
def _toRouteVo(self, Row, Permissions: list[RoutePermissionVO], Enabled: bool) -> RouteVO:
|
||||||
|
|||||||
Reference in New Issue
Block a user