feat(admin, admin-ui): 实现RuoYi风格后台菜单管理,优化媒体资源处理

新增服务端菜单校验与归一化逻辑,完善菜单相关Schema;前端实现条件渲染的菜单编辑器,支持搜索式图标选择器与父级树过滤。新增媒体URL统一处理工具修复管理端本地静态资源路径映射问题,更新全部相关文档、技术决策记录与测试用例。本次变更不影响WonderQ-MiniAPP端。
This commit is contained in:
duanshuwen committed 2026-08-27 09:08:05 +08:00
1 parent 6245159e7c
commit 4c245f0d1f
28 files changed
+625 -36

No files matched your search

+49 -11
View File
@@ -6,6 +6,7 @@ from sqlalchemy.orm import Session
from ..auth import get_actor_id, hash_password, require_admin
from ..database import get_db
from ..menu_rules import MENU_FIELDS, normalize_menu_values
from ..redis_session import AdminSessionStore, RedisUnavailableError, get_admin_session_store
from ..models import (
AdminDepartment,
@@ -87,6 +88,43 @@ def _ensure_ids_exist(db: Session, model, ids: list[str], label: str) -> None:
raise HTTPException(status_code=400, detail={"msg": f"{label}不存在", "errorCode": "REFERENCE_NOT_FOUND", "details": {"ids": missing}})
def _ensure_menu_parent(db: Session, menu_id: str | None, parent_id: str | None) -> None:
if not parent_id:
return
parent = db.get(AdminMenu, parent_id)
if not parent:
raise HTTPException(status_code=400, detail="父级菜单不存在")
if parent.type == "button":
raise HTTPException(status_code=400, detail="按钮菜单不能作为父级菜单")
seen: set[str] = set()
current: AdminMenu | None = parent
while current:
if current.id == menu_id:
raise HTTPException(status_code=400, detail="菜单不能移动到自己的子菜单下")
if current.id in seen:
break
seen.add(current.id)
current = db.get(AdminMenu, current.parentId) if current.parentId else None
def _normalized_menu_or_422(values: dict) -> dict:
try:
return normalize_menu_values(values)
except ValueError as exc:
raise HTTPException(status_code=422, detail=str(exc)) from exc
def _ensure_permission_available(db: Session, permission_code: str | None, menu_id: str | None = None) -> None:
if not permission_code:
return
stmt = select(AdminMenu.id).where(AdminMenu.permissionCode == permission_code)
if menu_id:
stmt = stmt.where(AdminMenu.id != menu_id)
if db.scalar(stmt):
raise HTTPException(status_code=409, detail="权限标识已存在")
def _invalidate_permissions(store: AdminSessionStore, user_ids: list[str] | None = None) -> None:
try:
store.invalidate_permission_cache(user_ids)
@@ -335,10 +373,10 @@ def delete_role(
@router.post("/menus", status_code=status.HTTP_201_CREATED)
def create_menu(body: AdminMenuCreate, request: Request, _user: AdminUser = Depends(require_permission("system:menu:create")), db: Session = Depends(get_db)):
if body.parentId and not db.get(AdminMenu, body.parentId):
raise HTTPException(status_code=400, detail="父级菜单不存在")
menu = AdminMenu(**body.model_dump())
menu.componentKey = body.componentKey
values = _normalized_menu_or_422(body.model_dump())
_ensure_menu_parent(db, None, values["parentId"])
_ensure_permission_available(db, values["permissionCode"])
menu = AdminMenu(**values)
db.add(menu)
db.flush()
_audit(db, request, "create", "admin_menu", menu.id, after=_menu_dict(menu))
@@ -351,13 +389,13 @@ def update_menu(menu_id: str, body: AdminMenuPatch, request: Request, _user: Adm
menu = db.get(AdminMenu, menu_id)
if not menu:
raise HTTPException(status_code=404, detail="菜单不存在")
if body.parentId == menu.id:
raise HTTPException(status_code=400, detail="菜单不能将自身设置为父级")
for field, value in body.model_dump(exclude_unset=True).items():
if field == "componentKey":
setattr(menu, "componentKey", value)
else:
setattr(menu, field, value)
values = {field: getattr(menu, field) for field in MENU_FIELDS}
values.update(body.model_dump(exclude_unset=True))
values = _normalized_menu_or_422(values)
_ensure_menu_parent(db, menu.id, values["parentId"])
_ensure_permission_available(db, values["permissionCode"], menu.id)
for field in MENU_FIELDS:
setattr(menu, field, values[field])
db.flush()
_audit(db, request, "update", "admin_menu", menu.id, after=_menu_dict(menu))
db.commit()