Files
hms/crates/erp-plugin/src/handler/data_handler.rs
iven a7342f83e9 feat(plugin): 数据范围查询基础设施 — get_data_scope + get_dept_members 辅助函数
- 新增 get_data_scope() 查询当前用户对指定权限的 data_scope 等级
- 新增 get_dept_members() 获取部门成员 ID 列表(预留递归部门树查询)
- 在 list_plugin_data handler 中标记 data_scope 注入点 TODO
- 这些基础设施函数将在前端 Chunk 4 完成完整集成
2026-04-17 10:49:57 +08:00

384 lines
12 KiB
Rust

use axum::Extension;
use axum::extract::{FromRef, Path, Query, State};
use axum::response::Json;
use uuid::Uuid;
use erp_core::error::AppError;
use erp_core::rbac::require_permission;
use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
use crate::data_dto::{
AggregateItem, AggregateQueryParams, CountQueryParams, CreatePluginDataReq,
PluginDataListParams, PluginDataResp, UpdatePluginDataReq,
};
use crate::data_service::{PluginDataService, resolve_manifest_id};
use crate::state::PluginState;
/// 获取当前用户对指定权限的 data_scope 等级
///
/// 查询 user_roles -> role_permissions -> permissions 链路,
/// 返回匹配权限的 data_scope 设置,默认 "all"。
async fn get_data_scope(
ctx: &TenantContext,
permission_code: &str,
db: &sea_orm::DatabaseConnection,
) -> Result<String, AppError> {
use sea_orm::{FromQueryResult, Statement};
#[derive(FromQueryResult)]
struct ScopeResult {
data_scope: Option<String>,
}
let result = ScopeResult::find_by_statement(Statement::from_sql_and_values(
sea_orm::DatabaseBackend::Postgres,
r#"SELECT rp.data_scope
FROM user_roles ur
JOIN role_permissions rp ON rp.role_id = ur.role_id
JOIN permissions p ON p.id = rp.permission_id
WHERE ur.user_id = $1 AND ur.tenant_id = $2 AND p.code = $3
LIMIT 1"#,
[
ctx.user_id.into(),
ctx.tenant_id.into(),
permission_code.into(),
],
))
.one(db)
.await
.map_err(|e| AppError::Internal(e.to_string()))?;
Ok(result
.and_then(|r| r.data_scope)
.unwrap_or_else(|| "all".to_string()))
}
/// 获取部门成员 ID 列表
///
/// 当前返回 TenantContext 中的 department_ids。
/// 未来实现递归查询部门树时将支持 include_sub_depts 参数。
async fn get_dept_members(
ctx: &TenantContext,
_include_sub_depts: bool,
) -> Vec<Uuid> {
// 当前 department_ids 为空时返回空列表
// 未来实现递归查询部门树
if ctx.department_ids.is_empty() {
return vec![];
}
ctx.department_ids.clone()
}
/// 计算插件数据操作所需的权限码
/// 格式:{manifest_id}.{entity}.{action},如 erp-crm.customer.list
fn compute_permission_code(manifest_id: &str, entity_name: &str, action: &str) -> String {
let action_suffix = match action {
"list" | "get" => "list",
_ => "manage",
};
format!("{}.{}.{}", manifest_id, entity_name, action_suffix)
}
#[utoipa::path(
get,
path = "/api/v1/plugins/{plugin_id}/{entity}",
params(PluginDataListParams),
responses(
(status = 200, description = "成功", body = ApiResponse<PaginatedResponse<PluginDataResp>>),
),
security(("bearer_auth" = [])),
tag = "插件数据"
)]
/// GET /api/v1/plugins/{plugin_id}/{entity} — 列表
pub async fn list_plugin_data<S>(
State(state): State<PluginState>,
Extension(ctx): Extension<TenantContext>,
Path((plugin_id, entity)): Path<(Uuid, String)>,
Query(params): Query<PluginDataListParams>,
) -> Result<Json<ApiResponse<PaginatedResponse<PluginDataResp>>>, AppError>
where
PluginState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
let manifest_id = resolve_manifest_id(plugin_id, ctx.tenant_id, &state.db).await?;
let fine_perm = compute_permission_code(&manifest_id, &entity, "list");
require_permission(&ctx, &fine_perm)?;
// TODO(data_scope): 此处注入行级数据权限过滤
// 1. 解析 entity 定义检查 data_scope == Some(true)
// 2. 调用 get_data_scope(&ctx, &fine_perm, &state.db) 获取当前用户的 scope 等级
// 3. 若 scope != "all",调用 get_dept_members 获取部门成员列表
// 4. 将 scope 条件合并到 filter 中传给 PluginDataService::list
// 参考: crates/erp-plugin/src/dynamic_table.rs build_data_scope_condition()
let page = params.page.unwrap_or(1);
let page_size = params.page_size.unwrap_or(20);
// 解析 filter JSON
let filter: Option<serde_json::Value> = params
.filter
.as_ref()
.and_then(|f| serde_json::from_str(f).ok());
let (items, total) = PluginDataService::list(
plugin_id,
&entity,
ctx.tenant_id,
page,
page_size,
&state.db,
filter,
params.search,
params.sort_by,
params.sort_order,
&state.entity_cache,
)
.await?;
Ok(Json(ApiResponse::ok(PaginatedResponse {
data: items,
total,
page,
page_size,
total_pages: (total as f64 / page_size as f64).ceil() as u64,
})))
}
#[utoipa::path(
post,
path = "/api/v1/plugins/{plugin_id}/{entity}",
request_body = CreatePluginDataReq,
responses(
(status = 200, description = "创建成功", body = ApiResponse<PluginDataResp>),
),
security(("bearer_auth" = [])),
tag = "插件数据"
)]
/// POST /api/v1/plugins/{plugin_id}/{entity} — 创建
pub async fn create_plugin_data<S>(
State(state): State<PluginState>,
Extension(ctx): Extension<TenantContext>,
Path((plugin_id, entity)): Path<(Uuid, String)>,
Json(req): Json<CreatePluginDataReq>,
) -> Result<Json<ApiResponse<PluginDataResp>>, AppError>
where
PluginState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
let manifest_id = resolve_manifest_id(plugin_id, ctx.tenant_id, &state.db).await?;
let fine_perm = compute_permission_code(&manifest_id, &entity, "create");
require_permission(&ctx, &fine_perm)?;
let result = PluginDataService::create(
plugin_id,
&entity,
ctx.tenant_id,
ctx.user_id,
req.data,
&state.db,
&state.event_bus,
)
.await?;
Ok(Json(ApiResponse::ok(result)))
}
#[utoipa::path(
get,
path = "/api/v1/plugins/{plugin_id}/{entity}/{id}",
responses(
(status = 200, description = "成功", body = ApiResponse<PluginDataResp>),
),
security(("bearer_auth" = [])),
tag = "插件数据"
)]
/// GET /api/v1/plugins/{plugin_id}/{entity}/{id} — 详情
pub async fn get_plugin_data<S>(
State(state): State<PluginState>,
Extension(ctx): Extension<TenantContext>,
Path((plugin_id, entity, id)): Path<(Uuid, String, Uuid)>,
) -> Result<Json<ApiResponse<PluginDataResp>>, AppError>
where
PluginState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
let manifest_id = resolve_manifest_id(plugin_id, ctx.tenant_id, &state.db).await?;
let fine_perm = compute_permission_code(&manifest_id, &entity, "get");
require_permission(&ctx, &fine_perm)?;
let result =
PluginDataService::get_by_id(plugin_id, &entity, id, ctx.tenant_id, &state.db).await?;
Ok(Json(ApiResponse::ok(result)))
}
#[utoipa::path(
put,
path = "/api/v1/plugins/{plugin_id}/{entity}/{id}",
request_body = UpdatePluginDataReq,
responses(
(status = 200, description = "更新成功", body = ApiResponse<PluginDataResp>),
),
security(("bearer_auth" = [])),
tag = "插件数据"
)]
/// PUT /api/v1/plugins/{plugin_id}/{entity}/{id} — 更新
pub async fn update_plugin_data<S>(
State(state): State<PluginState>,
Extension(ctx): Extension<TenantContext>,
Path((plugin_id, entity, id)): Path<(Uuid, String, Uuid)>,
Json(req): Json<UpdatePluginDataReq>,
) -> Result<Json<ApiResponse<PluginDataResp>>, AppError>
where
PluginState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
let manifest_id = resolve_manifest_id(plugin_id, ctx.tenant_id, &state.db).await?;
let fine_perm = compute_permission_code(&manifest_id, &entity, "update");
require_permission(&ctx, &fine_perm)?;
let result = PluginDataService::update(
plugin_id,
&entity,
id,
ctx.tenant_id,
ctx.user_id,
req.data,
req.version,
&state.db,
&state.event_bus,
)
.await?;
Ok(Json(ApiResponse::ok(result)))
}
#[utoipa::path(
delete,
path = "/api/v1/plugins/{plugin_id}/{entity}/{id}",
responses(
(status = 200, description = "删除成功"),
),
security(("bearer_auth" = [])),
tag = "插件数据"
)]
/// DELETE /api/v1/plugins/{plugin_id}/{entity}/{id} — 删除
pub async fn delete_plugin_data<S>(
State(state): State<PluginState>,
Extension(ctx): Extension<TenantContext>,
Path((plugin_id, entity, id)): Path<(Uuid, String, Uuid)>,
) -> Result<Json<ApiResponse<()>>, AppError>
where
PluginState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
let manifest_id = resolve_manifest_id(plugin_id, ctx.tenant_id, &state.db).await?;
let fine_perm = compute_permission_code(&manifest_id, &entity, "delete");
require_permission(&ctx, &fine_perm)?;
PluginDataService::delete(
plugin_id,
&entity,
id,
ctx.tenant_id,
&state.db,
&state.event_bus,
)
.await?;
Ok(Json(ApiResponse::ok(())))
}
#[utoipa::path(
get,
path = "/api/v1/plugins/{plugin_id}/{entity}/count",
params(CountQueryParams),
responses(
(status = 200, description = "成功", body = ApiResponse<u64>),
),
security(("bearer_auth" = [])),
tag = "插件数据"
)]
/// GET /api/v1/plugins/{plugin_id}/{entity}/count — 统计计数
pub async fn count_plugin_data<S>(
State(state): State<PluginState>,
Extension(ctx): Extension<TenantContext>,
Path((plugin_id, entity)): Path<(Uuid, String)>,
Query(params): Query<CountQueryParams>,
) -> Result<Json<ApiResponse<u64>>, AppError>
where
PluginState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
let manifest_id = resolve_manifest_id(plugin_id, ctx.tenant_id, &state.db).await?;
let fine_perm = compute_permission_code(&manifest_id, &entity, "list");
require_permission(&ctx, &fine_perm)?;
// 解析 filter JSON
let filter: Option<serde_json::Value> = params
.filter
.as_ref()
.and_then(|f| serde_json::from_str(f).ok());
let total = PluginDataService::count(
plugin_id,
&entity,
ctx.tenant_id,
&state.db,
filter,
params.search,
)
.await?;
Ok(Json(ApiResponse::ok(total)))
}
#[utoipa::path(
get,
path = "/api/v1/plugins/{plugin_id}/{entity}/aggregate",
params(AggregateQueryParams),
responses(
(status = 200, description = "成功", body = ApiResponse<Vec<AggregateItem>>),
),
security(("bearer_auth" = [])),
tag = "插件数据"
)]
/// GET /api/v1/plugins/{plugin_id}/{entity}/aggregate — 聚合查询
pub async fn aggregate_plugin_data<S>(
State(state): State<PluginState>,
Extension(ctx): Extension<TenantContext>,
Path((plugin_id, entity)): Path<(Uuid, String)>,
Query(params): Query<AggregateQueryParams>,
) -> Result<Json<ApiResponse<Vec<AggregateItem>>>, AppError>
where
PluginState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
let manifest_id = resolve_manifest_id(plugin_id, ctx.tenant_id, &state.db).await?;
let fine_perm = compute_permission_code(&manifest_id, &entity, "list");
require_permission(&ctx, &fine_perm)?;
// 解析 filter JSON
let filter: Option<serde_json::Value> = params
.filter
.as_ref()
.and_then(|f| serde_json::from_str(f).ok());
let rows = PluginDataService::aggregate(
plugin_id,
&entity,
ctx.tenant_id,
&state.db,
&params.group_by,
filter,
)
.await?;
let items = rows
.into_iter()
.map(|(key, count)| AggregateItem { key, count })
.collect();
Ok(Json(ApiResponse::ok(items)))
}