feat(admin): 实现动态路由菜单并修复rbac菜单树构建问题

- 修复rbac.py中菜单树构建逻辑,将`elif not menu.parentId`改为`else`,避免父菜单不存在时子菜单丢失
- 新增后端`/api/admin/system/routers`接口,作为动态路由菜单的API
- 前端新增`getRouters` API并整合到用户信息加载流程
- 添加菜单扁平化工具函数,更新路由注册逻辑以支持嵌套路由
- 新增相关测试用例并更新API和集成文档
This commit is contained in:
duanshuwen committed 2026-08-27 00:02:33 +08:00
1 parent f087663b5b
commit 2c8c327de7
12 files changed
+108 -5

No files matched your search

+1 -1
View File
@@ -38,7 +38,7 @@ def build_menu_tree(menus: Iterable[AdminMenu], visible_only: bool = True) -> li
node = nodes[menu.id]
if menu.parentId and menu.parentId in nodes:
nodes[menu.parentId]["children"].append(node)
elif not menu.parentId:
else:
roots.append(node)
return roots
+11
View File
@@ -111,6 +111,17 @@ def profile(
}
@router.get("/routers")
def routers(
user: AdminUser = Depends(require_admin),
db: Session = Depends(get_db),
store: AdminSessionStore = Depends(get_admin_session_store),
):
"""Return the authenticated user's visible menu tree for dynamic routing."""
context = build_admin_permission_context(user, db, store)
return {"code": 200, "msg": "success", "data": context["menus"]}
@router.get("/users")
def list_users(_user: AdminUser = Depends(require_permission("system:user:read")), db: Session = Depends(get_db)):
users = list(db.scalars(select(AdminUser).order_by(AdminUser.createdAt.desc())).all())
+10
View File
@@ -220,3 +220,13 @@ def test_dynamic_menu_tree_preserves_parent_order_and_button_permissions():
assert DATA_SCOPE_VALUES == {"all", "dept", "dept_and_children", "custom_dept", "self"}
assert [item["id"] for item in tree] == ["root"]
assert tree[0]["children"][0]["permission"] == "home:edit"
def test_dynamic_menu_tree_keeps_granted_menu_when_parent_is_not_in_scope():
menus = [
AdminMenu(id="page", parentId="missing-parent", name="受限页面", type="page", path="/restricted", sortOrder=10, isVisible=True, isActive=True),
]
tree = build_menu_tree(menus)
assert [item["id"] for item in tree] == ["page"]
@@ -257,6 +257,13 @@ def test_admin_requires_auth_for_protected_endpoint():
assert response.json() == {"code": 401, "msg": "请先登录后台", "data": None}
def test_admin_dynamic_routers_requires_admin_authentication():
response = TestClient(create_app()).get("/api/admin/system/routers")
assert response.status_code == 401
assert response.json() == {"code": 401, "msg": "请先登录后台", "data": None}
def test_admin_login_returns_token_and_user():
admin_user = AdminUser(
id="admin-test",