feat(config): 角色权限控制菜单可见性 + 医疗业务角色
Some checks failed
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled

- 修复 menu_service 角色过滤 bug: ctx.roles 存的是角色 code 而非 UUID,
  新增 resolve_role_ids() 方法通过 code 查找数据库中的角色 ID
- 创建 4 个医疗业务角色: 医生/护士/健康管理师/运营人员
- 重组菜单目录结构: 基础模块→工作台、业务模块→系统管理、健康管理→健康业务
- 菜单排序按功能域分组(患者医护/随访咨询/积分运营/内容运营/AI分析)
- 为各角色分配对应的菜单可见性和操作权限
This commit is contained in:
iven
2026-05-06 12:35:45 +08:00
parent 5fd8e88825
commit 570377a31f
5 changed files with 457 additions and 19 deletions

View File

@@ -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 {

View File

@@ -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)))
}

View File

@@ -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))