fix:[FL-218][角色管理列表接口返回 500]

Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
chengkai3
2026-06-20 16:13:47 +08:00
parent 93b580649a
commit fac00c0536
3 changed files with 127 additions and 24 deletions
+50 -24
View File
@@ -1012,14 +1012,25 @@ def _load_role_page(
limit_clause = "LIMIT :limit"
params["limit"] = normalized_limit
if role_source == "legacy":
from_clause = """
FROM user_role r
WHERE (
:keyword IS NULL
OR LOWER(r.id) LIKE :keyword
OR LOWER(r.name) LIKE :keyword
trimmed_keyword = keyword.strip() if keyword else ""
keyword_clause = ""
if trimmed_keyword:
keyword_clause = """
AND (
{role_keyword_predicates}
OR EXISTS (
{menu_exists_query}
)
)
"""
params["keyword"] = f"%{trimmed_keyword.lower()}%"
if role_source == "legacy":
role_keyword_predicates = """
LOWER(r.id) LIKE :keyword
OR LOWER(r.name) LIKE :keyword
"""
menu_exists_query = """
SELECT 1
FROM role_menu_rela rmr
JOIN menus m ON m.id::text = rmr.menu_id OR m.code = rmr.menu_id
@@ -1029,19 +1040,20 @@ def _load_role_page(
LOWER(m.code) LIKE :keyword
OR LOWER(m.name) LIKE :keyword
)
)
)
"""
from_clause = """
FROM user_role r
WHERE 1 = 1
{keyword_clause}
"""
select_clause = "SELECT r.id, r.name"
order_clause = "ORDER BY r.create_date DESC NULLS LAST, r.id ASC"
else:
from_clause = """
FROM roles r
WHERE (
:keyword IS NULL
OR LOWER(r.code) LIKE :keyword
role_keyword_predicates = """
LOWER(r.code) LIKE :keyword
OR LOWER(r.name) LIKE :keyword
OR EXISTS (
"""
menu_exists_query = """
SELECT 1
FROM role_menus rm
JOIN menus m ON m.id = rm.menu_id
@@ -1051,21 +1063,35 @@ def _load_role_page(
LOWER(m.code) LIKE :keyword
OR LOWER(m.name) LIKE :keyword
)
)
)
"""
from_clause = """
FROM roles r
WHERE 1 = 1
{keyword_clause}
"""
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
if keyword_clause:
keyword_clause = keyword_clause.format(
role_keyword_predicates=role_keyword_predicates,
menu_exists_query=menu_exists_query,
)
from_clause = from_clause.format(keyword_clause=keyword_clause)
query_params = {**params, "removed_menu_codes": tuple(REMOVED_MENU_CODES)} if trimmed_keyword else params
def role_page_stmt(sql: str):
stmt = text(sql)
if trimmed_keyword:
return stmt.bindparams(bindparam("removed_menu_codes", expanding=True))
return stmt
total = db.scalar(
text(f"SELECT COUNT(*) {from_clause}").bindparams(bindparam("removed_menu_codes", expanding=True)),
{**params, "removed_menu_codes": tuple(REMOVED_MENU_CODES)},
role_page_stmt(f"SELECT COUNT(*) {from_clause}"),
query_params,
) or 0
rows = db.execute(
text(
role_page_stmt(
f"""
{select_clause}
{from_clause}
@@ -1073,8 +1099,8 @@ def _load_role_page(
{limit_clause}
OFFSET :offset
"""
).bindparams(bindparam("removed_menu_codes", expanding=True)),
{**params, "removed_menu_codes": tuple(REMOVED_MENU_CODES)},
),
query_params,
).mappings().all()
return [dict(row) for row in rows], int(total)
@@ -7,6 +7,27 @@ def _role_row(index: int) -> dict[str, object]:
return {"id": f"role-{index}", "code": f"role-{index}", "name": f"Role {index}"}
class _EmptyRolePageRows:
def mappings(self):
return self
def all(self) -> list[dict[str, object]]:
return []
class _RolePageCaptureDb:
def __init__(self) -> None:
self.calls: list[tuple[str, str, dict[str, object]]] = []
def scalar(self, stmt, params=None) -> int:
self.calls.append(("scalar", str(stmt), dict(params or {})))
return 0
def execute(self, stmt, params=None) -> _EmptyRolePageRows:
self.calls.append(("execute", str(stmt), dict(params or {})))
return _EmptyRolePageRows()
def test_list_roles_returns_filtered_total_before_pagination(monkeypatch) -> None:
captured: dict[str, object] = {}
@@ -51,3 +72,40 @@ def test_list_roles_with_menus_paginates_roles_only(monkeypatch) -> None:
assert response.menus == []
assert response.menus_total == 2
assert captured == {"role_source": "modern", "keyword": None, "limit": 1, "offset": 1}
def test_load_role_page_without_keyword_omits_nullable_keyword_predicate() -> None:
for role_source in ("legacy", "modern"):
db = _RolePageCaptureDb()
rows, total = service._load_role_page(db, role_source=role_source, keyword=None, limit=20, offset=0)
assert rows == []
assert total == 0
assert len(db.calls) == 2
for _, sql, params in db.calls:
assert ":keyword" not in sql
assert ":keyword IS NULL" not in sql
assert "removed_menu_codes" not in params
def test_load_role_page_with_keyword_uses_normalized_like_filter() -> None:
role_predicates = {
"legacy": "LOWER(r.id) LIKE :keyword",
"modern": "LOWER(r.code) LIKE :keyword",
}
for role_source, role_predicate in role_predicates.items():
db = _RolePageCaptureDb()
rows, total = service._load_role_page(db, role_source=role_source, keyword=" Admin ", limit=20, offset=0)
assert rows == []
assert total == 0
assert len(db.calls) == 2
for _, sql, params in db.calls:
assert ":keyword IS NULL" not in sql
assert role_predicate in sql
assert "LOWER(r.name) LIKE :keyword" in sql
assert "LOWER(m.code) LIKE :keyword" in sql
assert params["keyword"] == "%admin%"
assert "removed_menu_codes" in params