[feat]:[FL-120][角色管理页面对齐用户管理分页交互]
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
@@ -47,19 +47,23 @@ router = APIRouter(prefix="/admin", tags=["admin"])
|
||||
@router.get("/roles", response_model=RoleListResponse)
|
||||
def get_roles(
|
||||
keyword: str | None = Query(default=None),
|
||||
limit: int = Query(default=50, ge=1, le=200),
|
||||
offset: int = Query(default=0, ge=0),
|
||||
_: CurrentUser = Depends(require_any_permission("role.read", "role.manage")),
|
||||
db: Session = Depends(get_db),
|
||||
) -> RoleListResponse:
|
||||
return list_roles(db, keyword=keyword)
|
||||
return list_roles(db, keyword=keyword, limit=limit, offset=offset)
|
||||
|
||||
|
||||
@router.get("/roles-with-menus", response_model=RolesWithMenusResponse)
|
||||
def get_roles_with_menus(
|
||||
keyword: str | None = Query(default=None),
|
||||
limit: int = Query(default=50, ge=1, le=200),
|
||||
offset: int = Query(default=0, ge=0),
|
||||
_: CurrentUser = Depends(require_any_permission("role.read", "role.manage")),
|
||||
db: Session = Depends(get_db),
|
||||
) -> RolesWithMenusResponse:
|
||||
return list_roles_with_menus(db, keyword=keyword)
|
||||
return list_roles_with_menus(db, keyword=keyword, limit=limit, offset=offset)
|
||||
|
||||
|
||||
@router.post("/roles", response_model=RolePublic)
|
||||
|
||||
@@ -111,9 +111,21 @@ PROTECTED_MENU_CODES = {
|
||||
}
|
||||
|
||||
|
||||
def list_roles(db: Session, keyword: str | None = None) -> RoleListResponse:
|
||||
def list_roles(
|
||||
db: Session,
|
||||
keyword: str | None = None,
|
||||
*,
|
||||
limit: int | None = None,
|
||||
offset: int = 0,
|
||||
) -> RoleListResponse:
|
||||
role_source = "legacy" if _legacy_role_table_exists(db) else "modern"
|
||||
rows = _load_role_rows(db, role_source=role_source, keyword=keyword)
|
||||
rows, total = _load_role_page(
|
||||
db,
|
||||
role_source=role_source,
|
||||
keyword=keyword,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
)
|
||||
role_ids = [str(row["id"]) for row in rows]
|
||||
role_menu_ids = _load_role_menu_ids_map(db, role_ids, role_source=role_source)
|
||||
menu_rows = _load_menus_map(
|
||||
@@ -131,20 +143,6 @@ def list_roles(db: Session, keyword: str | None = None) -> RoleListResponse:
|
||||
permission_codes = set(role_permission_codes.get(role_id, []))
|
||||
permission_codes.update(_permission_codes_from_menu_rows(menu_rows, menu_ids))
|
||||
|
||||
# Apply keyword filter on menu names if keyword provided
|
||||
if keyword:
|
||||
normalized_keyword = keyword.strip().lower()
|
||||
# Collect menu names for this role
|
||||
menu_names = " ".join(
|
||||
str(menu_rows.get(menu_id, {}).get("menu_name", ""))
|
||||
for menu_id in menu_ids
|
||||
)
|
||||
# Build search haystack
|
||||
haystack = f"{role_code} {row.get('name', '')} {menu_names}".lower()
|
||||
# Skip if keyword not found
|
||||
if normalized_keyword not in haystack:
|
||||
continue
|
||||
|
||||
items.append(
|
||||
RolePublic(
|
||||
id=role_id,
|
||||
@@ -154,7 +152,7 @@ def list_roles(db: Session, keyword: str | None = None) -> RoleListResponse:
|
||||
menu_ids=menu_ids,
|
||||
)
|
||||
)
|
||||
return RoleListResponse(items=items, total=len(items))
|
||||
return RoleListResponse(items=items, total=total)
|
||||
|
||||
|
||||
def get_role_by_id(db: Session, role_id: str) -> RolePublic | None:
|
||||
@@ -495,9 +493,15 @@ def list_menus(db: Session, keyword: str | None = None, status: str | None = Non
|
||||
return MenuListResponse(items=items, total=len(items))
|
||||
|
||||
|
||||
def list_roles_with_menus(db: Session, keyword: str | None = None) -> RolesWithMenusResponse:
|
||||
def list_roles_with_menus(
|
||||
db: Session,
|
||||
keyword: str | None = None,
|
||||
*,
|
||||
limit: int | None = None,
|
||||
offset: int = 0,
|
||||
) -> RolesWithMenusResponse:
|
||||
"""Get roles and menus in a single request to reduce network calls."""
|
||||
roles_response = list_roles(db, keyword=keyword)
|
||||
roles_response = list_roles(db, keyword=keyword, limit=limit, offset=offset)
|
||||
menus_response = list_menus(db)
|
||||
return RolesWithMenusResponse(
|
||||
roles=roles_response.items,
|
||||
@@ -992,42 +996,87 @@ def _load_menus_map(db: Session, menu_ids: list[str], *, role_source: str = "leg
|
||||
}
|
||||
|
||||
|
||||
def _load_role_rows(db: Session, *, role_source: str, keyword: str | None = None) -> list[dict[str, object]]:
|
||||
where_clause = ""
|
||||
params = {}
|
||||
|
||||
if keyword:
|
||||
normalized_keyword = f"%{keyword.strip().lower()}%"
|
||||
where_clause = "WHERE LOWER(id) LIKE :keyword OR LOWER(name) LIKE :keyword"
|
||||
params["keyword"] = normalized_keyword
|
||||
def _load_role_page(
|
||||
db: Session,
|
||||
*,
|
||||
role_source: str,
|
||||
keyword: str | None = None,
|
||||
limit: int | None = None,
|
||||
offset: int = 0,
|
||||
) -> tuple[list[dict[str, object]], int]:
|
||||
normalized_offset = max(offset, 0)
|
||||
normalized_limit = max(limit, 0) if limit is not None else None
|
||||
params: dict[str, object] = {"offset": normalized_offset}
|
||||
limit_clause = ""
|
||||
if normalized_limit is not None:
|
||||
limit_clause = "LIMIT :limit"
|
||||
params["limit"] = normalized_limit
|
||||
|
||||
if role_source == "legacy":
|
||||
rows = db.execute(
|
||||
text(
|
||||
f"""
|
||||
SELECT id, name
|
||||
FROM user_role
|
||||
{where_clause}
|
||||
ORDER BY create_date DESC NULLS LAST, id ASC
|
||||
"""
|
||||
),
|
||||
params
|
||||
).mappings().all()
|
||||
from_clause = """
|
||||
FROM user_role r
|
||||
WHERE (
|
||||
:keyword IS NULL
|
||||
OR LOWER(r.id) LIKE :keyword
|
||||
OR LOWER(r.name) LIKE :keyword
|
||||
OR EXISTS (
|
||||
SELECT 1
|
||||
FROM role_menu_rela rmr
|
||||
JOIN menus m ON m.id::text = rmr.menu_id OR m.code = rmr.menu_id
|
||||
WHERE rmr.role_id = r.id
|
||||
AND m.code NOT IN :removed_menu_codes
|
||||
AND (
|
||||
LOWER(m.code) LIKE :keyword
|
||||
OR LOWER(m.name) LIKE :keyword
|
||||
)
|
||||
)
|
||||
)
|
||||
"""
|
||||
select_clause = "SELECT r.id, r.name"
|
||||
order_clause = "ORDER BY r.create_date DESC NULLS LAST, r.id ASC"
|
||||
else:
|
||||
if keyword:
|
||||
where_clause = "WHERE LOWER(code) LIKE :keyword OR LOWER(name) LIKE :keyword"
|
||||
rows = db.execute(
|
||||
text(
|
||||
f"""
|
||||
SELECT id::text AS id, code, name
|
||||
FROM roles
|
||||
{where_clause}
|
||||
ORDER BY id ASC
|
||||
"""
|
||||
),
|
||||
params
|
||||
).mappings().all()
|
||||
return [dict(row) for row in rows]
|
||||
from_clause = """
|
||||
FROM roles r
|
||||
WHERE (
|
||||
:keyword IS NULL
|
||||
OR LOWER(r.code) LIKE :keyword
|
||||
OR LOWER(r.name) LIKE :keyword
|
||||
OR EXISTS (
|
||||
SELECT 1
|
||||
FROM role_menus rm
|
||||
JOIN menus m ON m.id = rm.menu_id
|
||||
WHERE rm.role_id = r.id
|
||||
AND m.code NOT IN :removed_menu_codes
|
||||
AND (
|
||||
LOWER(m.code) LIKE :keyword
|
||||
OR LOWER(m.name) LIKE :keyword
|
||||
)
|
||||
)
|
||||
)
|
||||
"""
|
||||
select_clause = "SELECT r.id::text AS id, r.code, r.name"
|
||||
order_clause = "ORDER BY r.id ASC"
|
||||
|
||||
trimmed_keyword = keyword.strip() if keyword else ""
|
||||
params["keyword"] = f"%{trimmed_keyword.lower()}%" if trimmed_keyword else None
|
||||
|
||||
total = db.scalar(
|
||||
text(f"SELECT COUNT(*) {from_clause}").bindparams(bindparam("removed_menu_codes", expanding=True)),
|
||||
{**params, "removed_menu_codes": tuple(REMOVED_MENU_CODES)},
|
||||
) or 0
|
||||
rows = db.execute(
|
||||
text(
|
||||
f"""
|
||||
{select_clause}
|
||||
{from_clause}
|
||||
{order_clause}
|
||||
{limit_clause}
|
||||
OFFSET :offset
|
||||
"""
|
||||
).bindparams(bindparam("removed_menu_codes", expanding=True)),
|
||||
{**params, "removed_menu_codes": tuple(REMOVED_MENU_CODES)},
|
||||
).mappings().all()
|
||||
return [dict(row) for row in rows], int(total)
|
||||
|
||||
|
||||
def _load_role_permission_codes_map(
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from app.services import legacy_admin_rbac_service as service
|
||||
|
||||
|
||||
def _role_row(index: int) -> dict[str, object]:
|
||||
return {"id": f"role-{index}", "code": f"role-{index}", "name": f"Role {index}"}
|
||||
|
||||
|
||||
def test_list_roles_returns_filtered_total_before_pagination(monkeypatch) -> None:
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
monkeypatch.setattr(service, "_legacy_role_table_exists", lambda db: True)
|
||||
|
||||
def load_page(db, *, role_source, keyword=None, limit=None, offset=0):
|
||||
captured.update({"role_source": role_source, "keyword": keyword, "limit": limit, "offset": offset})
|
||||
return [_role_row(3), _role_row(4)], 5
|
||||
|
||||
monkeypatch.setattr(service, "_load_role_page", load_page)
|
||||
monkeypatch.setattr(service, "_load_role_menu_ids_map", lambda db, role_ids, *, role_source: {role_id: [] for role_id in role_ids})
|
||||
monkeypatch.setattr(service, "_load_menus_map", lambda db, menu_ids, *, role_source: {})
|
||||
monkeypatch.setattr(service, "_load_role_permission_codes_map", lambda db, role_ids, *, role_source: {role_id: [] for role_id in role_ids})
|
||||
|
||||
response = service.list_roles(object(), keyword="role", limit=2, offset=2)
|
||||
|
||||
assert response.total == 5
|
||||
assert [role.id for role in response.items] == ["role-3", "role-4"]
|
||||
assert captured == {"role_source": "legacy", "keyword": "role", "limit": 2, "offset": 2}
|
||||
|
||||
|
||||
def test_list_roles_with_menus_paginates_roles_only(monkeypatch) -> None:
|
||||
captured: dict[str, object] = {}
|
||||
menu_response = service.MenuListResponse(items=[], total=2)
|
||||
|
||||
monkeypatch.setattr(service, "_legacy_role_table_exists", lambda db: False)
|
||||
|
||||
def load_page(db, *, role_source, keyword=None, limit=None, offset=0):
|
||||
captured.update({"role_source": role_source, "keyword": keyword, "limit": limit, "offset": offset})
|
||||
return [_role_row(2)], 3
|
||||
|
||||
monkeypatch.setattr(service, "_load_role_page", load_page)
|
||||
monkeypatch.setattr(service, "_load_role_menu_ids_map", lambda db, role_ids, *, role_source: {role_id: [] for role_id in role_ids})
|
||||
monkeypatch.setattr(service, "_load_menus_map", lambda db, menu_ids, *, role_source: {})
|
||||
monkeypatch.setattr(service, "_load_role_permission_codes_map", lambda db, role_ids, *, role_source: {role_id: [] for role_id in role_ids})
|
||||
monkeypatch.setattr(service, "list_menus", lambda db: menu_response)
|
||||
|
||||
response = service.list_roles_with_menus(object(), limit=1, offset=1)
|
||||
|
||||
assert response.roles_total == 3
|
||||
assert [role.id for role in response.roles] == ["role-2"]
|
||||
assert response.menus == []
|
||||
assert response.menus_total == 2
|
||||
assert captured == {"role_source": "modern", "keyword": None, "limit": 1, "offset": 1}
|
||||
Reference in New Issue
Block a user