修复菜单管理在modern表结构下删除失败
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
@@ -530,33 +530,78 @@ def update_menu(db: Session, menu_id: str, payload: MenuUpdateRequest) -> MenuPu
|
||||
|
||||
|
||||
def delete_menu(db: Session, menu_id: str) -> bool:
|
||||
menu = get_menu_by_id(db, menu_id)
|
||||
if not menu:
|
||||
return False
|
||||
if menu.code in PROTECTED_MENU_CODES:
|
||||
normalized_menu_id = menu_id.strip()
|
||||
if not normalized_menu_id:
|
||||
return False
|
||||
|
||||
child_exists = db.scalar(text("SELECT menu_id FROM menu WHERE parent_id = :parent_id LIMIT 1"), {"parent_id": menu.id})
|
||||
if child_exists:
|
||||
return False
|
||||
menu_source = "legacy" if _legacy_menu_table_exists(db) else "modern"
|
||||
|
||||
impacted_user_ids = _get_users_with_menu_access(db, menu.id)
|
||||
try:
|
||||
db.execute(text("DELETE FROM role_menu_rela WHERE menu_id = :menu_id"), {"menu_id": menu.id})
|
||||
db.execute(text("DELETE FROM menu WHERE menu_id = :menu_id"), {"menu_id": menu.id})
|
||||
db.commit()
|
||||
except SQLAlchemyError:
|
||||
db.rollback()
|
||||
return False
|
||||
if menu_source == "legacy":
|
||||
menu = get_menu_by_id(db, normalized_menu_id)
|
||||
if not menu:
|
||||
return False
|
||||
if menu.code in PROTECTED_MENU_CODES:
|
||||
return False
|
||||
|
||||
child_exists = db.scalar(text("SELECT menu_id FROM menu WHERE parent_id = :parent_id LIMIT 1"), {"parent_id": menu.id})
|
||||
if child_exists:
|
||||
return False
|
||||
|
||||
impacted_user_ids = _get_users_with_menu_access(db, menu.id, role_source=menu_source)
|
||||
try:
|
||||
db.execute(text("DELETE FROM role_menu_rela WHERE menu_id = :menu_id"), {"menu_id": menu.id})
|
||||
db.execute(text("DELETE FROM menu WHERE menu_id = :menu_id"), {"menu_id": menu.id})
|
||||
db.commit()
|
||||
except SQLAlchemyError:
|
||||
db.rollback()
|
||||
return False
|
||||
|
||||
deleted_menu_id = menu.id
|
||||
deleted_menu_code = menu.code
|
||||
else:
|
||||
menu_row = db.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT id::text AS menu_id, code AS menu_code
|
||||
FROM menus
|
||||
WHERE id::text = :menu_id OR code = :menu_id
|
||||
LIMIT 1
|
||||
"""
|
||||
),
|
||||
{"menu_id": normalized_menu_id},
|
||||
).mappings().first()
|
||||
if not menu_row:
|
||||
return False
|
||||
|
||||
resolved_menu_id = str(menu_row["menu_id"])
|
||||
resolved_menu_code = str(menu_row.get("menu_code") or "").strip()
|
||||
if not resolved_menu_code or resolved_menu_code in PROTECTED_MENU_CODES:
|
||||
return False
|
||||
|
||||
child_exists = db.scalar(text("SELECT id::text FROM menus WHERE parent_id::text = :parent_id LIMIT 1"), {"parent_id": resolved_menu_id})
|
||||
if child_exists:
|
||||
return False
|
||||
|
||||
impacted_user_ids = _get_users_with_menu_access(db, resolved_menu_id, role_source=menu_source)
|
||||
try:
|
||||
db.execute(text("DELETE FROM role_menus WHERE menu_id::text = :menu_id"), {"menu_id": resolved_menu_id})
|
||||
db.execute(text("DELETE FROM menus WHERE id::text = :menu_id"), {"menu_id": resolved_menu_id})
|
||||
db.commit()
|
||||
except SQLAlchemyError:
|
||||
db.rollback()
|
||||
return False
|
||||
|
||||
deleted_menu_id = resolved_menu_id
|
||||
deleted_menu_code = resolved_menu_code
|
||||
|
||||
queue_users_auth_refresh(db, impacted_user_ids)
|
||||
_fire_and_forget(
|
||||
publish_topic(
|
||||
"admin.menus",
|
||||
name="menus.changed",
|
||||
payload={"action": "deleted", "menu_id": menu.id, "menu_code": menu.code},
|
||||
payload={"action": "deleted", "menu_id": deleted_menu_id, "menu_code": deleted_menu_code},
|
||||
requires_refetch=["/api/v1/admin/menus", "/api/v1/admin/me/menus"],
|
||||
dedupe_key=f"menus:deleted:{menu.id}",
|
||||
dedupe_key=f"menus:deleted:{deleted_menu_id}",
|
||||
)
|
||||
)
|
||||
return True
|
||||
@@ -777,6 +822,14 @@ def _legacy_role_table_exists(db: Session) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def _legacy_menu_table_exists(db: Session) -> bool:
|
||||
try:
|
||||
return bool(db.scalar(text("SELECT to_regclass('public.menu')")))
|
||||
except SQLAlchemyError:
|
||||
db.rollback()
|
||||
return False
|
||||
|
||||
|
||||
def _permission_codes_from_menu_rows(
|
||||
menu_rows: dict[str, dict[str, object]],
|
||||
menu_ids: list[str],
|
||||
@@ -892,19 +945,32 @@ def _get_role_user_ids(db: Session, role_id: str) -> list[str]:
|
||||
return sorted({str(row[0]) for row in rows})
|
||||
|
||||
|
||||
def _get_users_with_menu_access(db: Session, menu_id: str) -> list[str]:
|
||||
def _get_users_with_menu_access(db: Session, menu_id: str, *, role_source: str = "legacy") -> list[str]:
|
||||
try:
|
||||
rows = db.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT DISTINCT urr.user_id
|
||||
FROM role_menu_rela rmr
|
||||
JOIN user_role_rela urr ON urr.role_id = rmr.role_id
|
||||
WHERE rmr.menu_id = :menu_id
|
||||
"""
|
||||
),
|
||||
{"menu_id": menu_id},
|
||||
).all()
|
||||
if role_source == "legacy":
|
||||
rows = db.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT DISTINCT urr.user_id
|
||||
FROM role_menu_rela rmr
|
||||
JOIN user_role_rela urr ON urr.role_id = rmr.role_id
|
||||
WHERE rmr.menu_id = :menu_id
|
||||
"""
|
||||
),
|
||||
{"menu_id": menu_id},
|
||||
).all()
|
||||
else:
|
||||
rows = db.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT DISTINCT ur.user_id
|
||||
FROM role_menus rm
|
||||
JOIN user_roles ur ON ur.role_id = rm.role_id
|
||||
WHERE rm.menu_id::text = :menu_id
|
||||
"""
|
||||
),
|
||||
{"menu_id": menu_id},
|
||||
).all()
|
||||
except SQLAlchemyError:
|
||||
db.rollback()
|
||||
return []
|
||||
|
||||
@@ -259,6 +259,28 @@
|
||||
- 文件:`web/src/app/admin/workers/page.tsx`
|
||||
- Worker 列表与单 worker 任务请求均改为 `forceRefresh=true`,页面默认拉取实时数据。
|
||||
|
||||
## Work Log - 菜单管理支持删除(2026-05-03)
|
||||
|
||||
- 背景:
|
||||
- Issue `FL-185` 反馈“菜单管理要支持删除”。
|
||||
- 前端已有删除入口,但后端删除逻辑主要按 legacy 表(`menu` / `role_menu_rela`)执行,在 modern 表结构(`menus` / `role_menus`)下会删除失败。
|
||||
|
||||
- 本次改动(最小闭环):
|
||||
- 文件:`api/app/services/legacy_admin_rbac_service.py`
|
||||
- `delete_menu` 增加数据源分支:
|
||||
- legacy:保留原有删除流程(`menu` + `role_menu_rela`);
|
||||
- modern:新增 `menus` + `role_menus` 删除路径,支持按 `id` 或 `code` 解析菜单。
|
||||
- 新增 `_legacy_menu_table_exists`,通过 `to_regclass('public.menu')` 判断是否走 legacy 分支。
|
||||
- 扩展 `_get_users_with_menu_access`,支持按 `role_source` 查询 legacy/modern 角色菜单关联,确保删除后权限缓存刷新用户范围正确。
|
||||
- 保持既有保护规则不变:
|
||||
- 受保护菜单编码仍禁止删除;
|
||||
- 存在子菜单时仍禁止删除;
|
||||
- 删除后继续发布 `admin.menus` 变更事件,前端列表与侧边菜单自动刷新。
|
||||
|
||||
- 风险与影响:
|
||||
- 影响面仅后台菜单删除服务层;菜单新增、编辑、查询与权限模型未改。
|
||||
- 若目标菜单存在子菜单或在保护名单中,行为仍与历史一致(返回不可删除)。
|
||||
|
||||
- 验证:
|
||||
- 代码路径校对:前端监控页 -> `/api/v1/admin/flower/workers` -> Flower `/api/workers` 代理链路保持不变,仅调整刷新策略与容错。
|
||||
- 自动注册路径校对:`worker_ready/heartbeat_sent/worker_shutdown` 仍通过 `worker_registry_service` 入库,仅修正 worker 名标准化逻辑。
|
||||
|
||||
Reference in New Issue
Block a user