feat(config): 角色权限控制菜单可见性 + 医疗业务角色
- 修复 menu_service 角色过滤 bug: ctx.roles 存的是角色 code 而非 UUID, 新增 resolve_role_ids() 方法通过 code 查找数据库中的角色 ID - 创建 4 个医疗业务角色: 医生/护士/健康管理师/运营人员 - 重组菜单目录结构: 基础模块→工作台、业务模块→系统管理、健康管理→健康业务 - 菜单排序按功能域分组(患者医护/随访咨询/积分运营/内容运营/AI分析) - 为各角色分配对应的菜单可见性和操作权限
This commit is contained in:
@@ -28,6 +28,12 @@ impl From<sea_orm::TransactionError<ConfigError>> for ConfigError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<sea_orm::DbErr> for ConfigError {
|
||||
fn from(err: sea_orm::DbErr) -> Self {
|
||||
ConfigError::Validation(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ConfigError> for AppError {
|
||||
fn from(err: ConfigError) -> Self {
|
||||
match err {
|
||||
|
||||
@@ -36,13 +36,7 @@ where
|
||||
{
|
||||
require_permission(&ctx, "menu.list")?;
|
||||
|
||||
let role_ids: Vec<Uuid> = ctx
|
||||
.roles
|
||||
.iter()
|
||||
.filter_map(|r| Uuid::parse_str(r).ok())
|
||||
.collect();
|
||||
|
||||
let menus = MenuService::get_menu_tree(ctx.tenant_id, &role_ids, &state.db).await?;
|
||||
let menus = MenuService::get_menu_tree(ctx.tenant_id, &ctx.roles, &state.db).await?;
|
||||
|
||||
Ok(JsonResponse(ApiResponse::ok(menus)))
|
||||
}
|
||||
@@ -257,14 +251,7 @@ where
|
||||
ConfigState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
let role_ids: Vec<Uuid> = ctx
|
||||
.roles
|
||||
.iter()
|
||||
.filter_map(|r| Uuid::parse_str(r).ok())
|
||||
.collect();
|
||||
|
||||
// 如果用户有角色关联菜单,按角色过滤;否则返回全部(admin 兜底)
|
||||
let menus = MenuService::get_menu_tree(ctx.tenant_id, &role_ids, &state.db).await?;
|
||||
let menus = MenuService::get_menu_tree(ctx.tenant_id, &ctx.roles, &state.db).await?;
|
||||
|
||||
Ok(JsonResponse(ApiResponse::ok(menus)))
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use chrono::Utc;
|
||||
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QueryOrder, Set};
|
||||
use sea_orm::{ActiveModelTrait, ColumnTrait, ConnectionTrait, EntityTrait, QueryFilter, QueryOrder, Set};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::dto::{CreateMenuReq, MenuResp};
|
||||
@@ -17,15 +17,49 @@ use erp_core::events::EventBus;
|
||||
pub struct MenuService;
|
||||
|
||||
impl MenuService {
|
||||
/// 通过角色 code 列表查找对应的角色 ID 列表。
|
||||
async fn resolve_role_ids(
|
||||
tenant_id: Uuid,
|
||||
role_codes: &[String],
|
||||
db: &sea_orm::DatabaseConnection,
|
||||
) -> ConfigResult<Vec<Uuid>> {
|
||||
if role_codes.is_empty() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
let codes_csv: String = role_codes
|
||||
.iter()
|
||||
.map(|c| format!("'{}'", c.replace('\'', "''")))
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
let sql = format!(
|
||||
"SELECT id FROM roles WHERE tenant_id = '{}' AND code IN ({}) AND deleted_at IS NULL",
|
||||
tenant_id, codes_csv
|
||||
);
|
||||
let stmt = sea_orm::Statement::from_string(sea_orm::DatabaseBackend::Postgres, sql);
|
||||
let rows = db.query_all(stmt).await?;
|
||||
|
||||
Ok(rows
|
||||
.into_iter()
|
||||
.filter_map(|row| {
|
||||
let id: Uuid = row.try_get_by_index(0).ok()?;
|
||||
Some(id)
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// 获取当前租户下指定角色可见的菜单树。
|
||||
///
|
||||
/// 如果 `role_ids` 非空,仅返回这些角色关联的菜单;
|
||||
/// 否则返回租户全部菜单。结果按 `sort_order` 排列并组装为树形结构。
|
||||
/// `role_codes` 为当前用户的角色 code 列表(如 ["admin"]、["doctor"])。
|
||||
/// 方法内部将 code 转换为 ID,再通过 menu_roles 表过滤。
|
||||
/// 如果角色没有任何菜单关联,返回全部菜单(admin 兜底)。
|
||||
pub async fn get_menu_tree(
|
||||
tenant_id: Uuid,
|
||||
role_ids: &[Uuid],
|
||||
role_codes: &[String],
|
||||
db: &sea_orm::DatabaseConnection,
|
||||
) -> ConfigResult<Vec<MenuResp>> {
|
||||
// 0. 将角色 code 转换为 UUID
|
||||
let role_ids = Self::resolve_role_ids(tenant_id, role_codes, db).await?;
|
||||
|
||||
// 1. 查询租户下所有未删除的菜单,按 sort_order 排序
|
||||
let all_menus = menu::Entity::find()
|
||||
.filter(menu::Column::TenantId.eq(tenant_id))
|
||||
|
||||
Reference in New Issue
Block a user