diff --git a/api/app/services/legacy_admin_rbac_service.py b/api/app/services/legacy_admin_rbac_service.py index 99ea127..80ffa11 100644 --- a/api/app/services/legacy_admin_rbac_service.py +++ b/api/app/services/legacy_admin_rbac_service.py @@ -316,19 +316,49 @@ def update_role(db: Session, role_id: str, payload: RoleUpdateRequest) -> RolePu def delete_role(db: Session, role_id: str) -> bool: role_id = role_id.strip() - if not role_id or role_id in PROTECTED_ROLE_IDS: - return False - exists = db.scalar(text("SELECT id FROM user_role WHERE id = :id"), {"id": role_id}) - if not exists: + if not role_id: return False + role_source = "legacy" if _legacy_role_table_exists(db) else "modern" + resolved_role_id = role_id + resolved_role_code = role_id + if role_source == "legacy": + if role_id in PROTECTED_ROLE_IDS: + return False + exists = db.scalar(text("SELECT id FROM user_role WHERE id = :id"), {"id": role_id}) + if not exists: + return False + else: + role_row = db.execute( + text( + """ + SELECT id::text AS id, code + FROM roles + WHERE id::text = :id OR code = :id + LIMIT 1 + """ + ), + {"id": role_id}, + ).mappings().first() + if not role_row: + return False + resolved_role_id = str(role_row["id"]) + resolved_role_code = str(role_row.get("code") or resolved_role_id).strip() or resolved_role_id + if resolved_role_code in PROTECTED_ROLE_IDS: + return False - impacted_user_ids = _get_role_user_ids(db, role_id) + impacted_user_ids = _get_role_user_ids(db, resolved_role_id) has_user_role_relation = _legacy_user_role_relation_exists(db) try: - db.execute(text("DELETE FROM role_menu_rela WHERE role_id = :role_id"), {"role_id": role_id}) - if has_user_role_relation: - db.execute(text("DELETE FROM user_role_rela WHERE role_id = :role_id"), {"role_id": role_id}) - db.execute(text("DELETE FROM user_role WHERE id = :id"), {"id": role_id}) + if role_source == "legacy": + db.execute(text("DELETE FROM role_menu_rela WHERE role_id = :role_id"), {"role_id": resolved_role_id}) + if has_user_role_relation: + db.execute(text("DELETE FROM user_role_rela WHERE role_id = :role_id"), {"role_id": resolved_role_id}) + db.execute(text("DELETE FROM user_role WHERE id = :id"), {"id": resolved_role_id}) + else: + db.execute(text("DELETE FROM role_menus WHERE role_id::text = :role_id"), {"role_id": resolved_role_id}) + db.execute(text("DELETE FROM role_permissions WHERE role_id::text = :role_id"), {"role_id": resolved_role_id}) + db.execute(text("DELETE FROM user_roles WHERE role_id::text = :role_id"), {"role_id": resolved_role_id}) + db.execute(text("DELETE FROM roles WHERE id::text = :id"), {"id": resolved_role_id}) db.commit() except SQLAlchemyError: db.rollback() @@ -339,18 +369,18 @@ def delete_role(db: Session, role_id: str) -> bool: publish_topic( "admin.roles", name="roles.changed", - payload={"action": "deleted", "role_id": role_id, "role_code": role_id}, + payload={"action": "deleted", "role_id": resolved_role_id, "role_code": resolved_role_code}, requires_refetch=["/api/v1/admin/roles"], - dedupe_key=f"roles:deleted:{role_id}", + dedupe_key=f"roles:deleted:{resolved_role_id}", ) ) _fire_and_forget( publish_topic( "admin.menus", name="menus.changed", - payload={"action": "role_deleted", "role_id": role_id, "role_code": role_id}, + payload={"action": "role_deleted", "role_id": resolved_role_id, "role_code": resolved_role_code}, requires_refetch=["/api/v1/admin/me/menus"], - dedupe_key=f"menus:role_deleted:{role_id}", + dedupe_key=f"menus:role_deleted:{resolved_role_id}", ) ) return True diff --git a/memory/2026-05-07.md b/memory/2026-05-07.md new file mode 100644 index 0000000..75f5212 --- /dev/null +++ b/memory/2026-05-07.md @@ -0,0 +1,25 @@ +## Work Log - 角色管理支持删除(2026-05-07) + +- 背景: + - Issue `FL-203` 反馈“角色管理要支持删除”。 + +- 根因: + - `api/app/services/legacy_admin_rbac_service.py` 的 `delete_role` 只按 legacy 表(`user_role`/`role_menu_rela`)执行删除。 + - 当环境使用 modern RBAC 表(`roles`/`role_menus`/`role_permissions`/`user_roles`)时,删除接口会判定角色不存在或删除失败。 + +- 本次改动(最小闭环): + - 文件:`api/app/services/legacy_admin_rbac_service.py` + - 调整 `delete_role` 逻辑为与列表/更新一致的双模式处理: + - 自动识别 `legacy`/`modern` 角色来源。 + - `modern` 模式下支持按 `id` 或 `code` 定位角色。 + - 删除时同步清理 `role_menus`、`role_permissions`、`user_roles` 关联并删除 `roles` 主记录。 + - 保护角色校验从仅 legacy `id` 扩展为 modern `code` 校验,保持 `admin/user/sys_mgr` 不可删。 + - 推送事件 `role_id/role_code` 改为使用解析后的真实值,避免前端订阅端收到错误标识。 + +- 验证: + - 未执行编译与自动化测试(遵循任务约束:不做编译检查,不安装依赖)。 + - 通过代码链路核对,`DELETE /api/v1/admin/roles/{role_id}` 在 legacy 与 modern 两种表结构下均有完整删除路径。 + +- 风险与影响: + - 影响面限定在角色删除服务逻辑。 + - 角色列表、创建、编辑接口行为未改动。