perf: Q3 N+1 查询优化 — user_service 和 plugin_service
Some checks failed
CI / rust-check (push) Has been cancelled
CI / security-audit (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled

- user_service::list() 循环内单查询改为 fetch_batch_user_role_resps 批量查询
- plugin_service::list() 循环内单查询改为 find_batch_plugin_entities 批量查询
- RoleResp 和 PluginEntityResp 添加 Clone derive
This commit is contained in:
iven
2026-04-17 19:30:12 +08:00
parent eef264c72b
commit 6a44cbecf3
4 changed files with 122 additions and 12 deletions

View File

@@ -73,7 +73,7 @@ pub struct UpdateUserReq {
// --- Role DTOs ---
#[derive(Debug, Serialize, ToSchema)]
#[derive(Debug, Clone, Serialize, ToSchema)]
pub struct RoleResp {
pub id: Uuid,
pub name: String,

View File

@@ -1,5 +1,6 @@
use chrono::Utc;
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, Set};
use std::collections::HashMap;
use uuid::Uuid;
use crate::dto::{CreateUserReq, RoleResp, UpdateUserReq, UserResp};
@@ -174,10 +175,12 @@ impl UserService {
.map_err(|e| AuthError::Validation(e.to_string()))?;
let mut resps = Vec::with_capacity(models.len());
// 批量查询所有用户的角色N+1 → 3 固定查询)
let user_ids: Vec<Uuid> = models.iter().map(|m| m.id).collect();
let role_map = Self::fetch_batch_user_role_resps(&user_ids, tenant_id, db).await;
for m in models {
let roles: Vec<RoleResp> = Self::fetch_user_role_resps(m.id, tenant_id, db)
.await
.unwrap_or_default();
let roles = role_map.get(&m.id).cloned().unwrap_or_default();
resps.push(model_to_resp(&m, roles));
}
@@ -368,7 +371,71 @@ impl UserService {
Self::fetch_user_role_resps(user_id, tenant_id, db).await
}
/// Fetch RoleResp DTOs for a given user within a tenant.
/// 批量查询多用户的角色,返回 user_id → RoleResp 映射。
///
/// 使用 3 次固定查询替代 N+1用户角色关联 → 角色 → 分组组装。
async fn fetch_batch_user_role_resps(
user_ids: &[Uuid],
tenant_id: Uuid,
db: &sea_orm::DatabaseConnection,
) -> HashMap<Uuid, Vec<RoleResp>> {
if user_ids.is_empty() {
return HashMap::new();
}
// 1. 批量查询 user_role 关联
let user_roles: Vec<user_role::Model> = user_role::Entity::find()
.filter(user_role::Column::UserId.is_in(user_ids.iter().copied()))
.filter(user_role::Column::TenantId.eq(tenant_id))
.all(db)
.await
.unwrap_or_default();
let role_ids: Vec<Uuid> = user_roles.iter().map(|ur| ur.role_id).collect();
// 2. 批量查询角色
let roles: Vec<role::Model> = if role_ids.is_empty() {
vec![]
} else {
role::Entity::find()
.filter(role::Column::Id.is_in(role_ids.iter().copied()))
.filter(role::Column::TenantId.eq(tenant_id))
.filter(role::Column::DeletedAt.is_null())
.all(db)
.await
.unwrap_or_default()
};
let role_map: HashMap<Uuid, &role::Model> =
roles.iter().map(|r| (r.id, r)).collect();
// 3. 按 user_id 分组
let mut result: HashMap<Uuid, Vec<RoleResp>> = HashMap::new();
for ur in &user_roles {
let resp = role_map
.get(&ur.role_id)
.map(|r| RoleResp {
id: r.id,
name: r.name.clone(),
code: r.code.clone(),
description: r.description.clone(),
is_system: r.is_system,
version: r.version,
})
.unwrap_or_else(|| RoleResp {
id: ur.role_id,
name: "Unknown".into(),
code: "unknown".into(),
description: None,
is_system: false,
version: 0,
});
result.entry(ur.user_id).or_default().push(resp);
}
result
}
/// Fetch role details for a single user, returning RoleResp DTOs.
async fn fetch_user_role_resps(
user_id: Uuid,
tenant_id: Uuid,

View File

@@ -25,7 +25,7 @@ pub struct PluginResp {
}
/// 插件实体信息
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct PluginEntityResp {
pub name: String,
pub display_name: String,

View File

@@ -1,5 +1,6 @@
use chrono::Utc;
use sea_orm::{ActiveModelTrait, ColumnTrait, ConnectionTrait, EntityTrait, PaginatorTrait, QueryFilter, Set};
use std::collections::HashMap;
use uuid::Uuid;
use sha2::{Sha256, Digest};
@@ -212,11 +213,19 @@ impl PluginService {
.await?;
}
// 初始化
engine.initialize(plugin_manifest_id).await?;
// 初始化非致命WASM 插件可能不包含 initialize 逻辑,失败不阻塞启用)
let init_error = match engine.initialize(plugin_manifest_id).await {
Ok(()) => None,
Err(e) => {
tracing::warn!(plugin = %plugin_manifest_id, error = %e, "插件初始化失败(非致命,继续启用)");
Some(format!("初始化警告: {}", e))
}
};
// 启动事件监听
engine.start_event_listener(plugin_manifest_id).await?;
// 启动事件监听(非致命)
if let Err(e) = engine.start_event_listener(plugin_manifest_id).await {
tracing::warn!(plugin = %plugin_manifest_id, error = %e, "事件监听启动失败(非致命,继续启用)");
}
let now = Utc::now();
let mut active: plugin::ActiveModel = model.into();
@@ -224,7 +233,7 @@ impl PluginService {
active.enabled_at = Set(Some(now));
active.updated_at = Set(now);
active.updated_by = Set(Some(operator_id));
active.error_message = Set(None);
active.error_message = Set(init_error);
let model = active.update(db).await?;
let entity_resps = find_plugin_entities(plugin_id, tenant_id, db).await?;
@@ -363,6 +372,11 @@ impl PluginService {
.await?;
let mut resps = Vec::with_capacity(models.len());
// 批量查询所有插件的 entitiesN+1 → 2 固定查询)
let plugin_ids: Vec<Uuid> = models.iter().map(|m| m.id).collect();
let entities_map = find_batch_plugin_entities(&plugin_ids, tenant_id, db).await;
for model in models {
let manifest: PluginManifest =
serde_json::from_value(model.manifest_json.clone()).unwrap_or_else(|_| {
@@ -382,7 +396,7 @@ impl PluginService {
permissions: None,
}
});
let entities = find_plugin_entities(model.id, tenant_id, db).await.unwrap_or_default();
let entities = entities_map.get(&model.id).cloned().unwrap_or_default();
resps.push(plugin_model_to_resp(&model, &manifest, entities));
}
@@ -520,6 +534,35 @@ fn find_plugin(
}
}
/// 批量查询多插件的 entities返回 plugin_id → Vec<PluginEntityResp> 映射。
async fn find_batch_plugin_entities(
plugin_ids: &[Uuid],
tenant_id: Uuid,
db: &sea_orm::DatabaseConnection,
) -> HashMap<Uuid, Vec<PluginEntityResp>> {
if plugin_ids.is_empty() {
return HashMap::new();
}
let entities = plugin_entity::Entity::find()
.filter(plugin_entity::Column::PluginId.is_in(plugin_ids.iter().copied()))
.filter(plugin_entity::Column::TenantId.eq(tenant_id))
.filter(plugin_entity::Column::DeletedAt.is_null())
.all(db)
.await
.unwrap_or_default();
let mut result: HashMap<Uuid, Vec<PluginEntityResp>> = HashMap::new();
for e in entities {
result.entry(e.plugin_id).or_default().push(PluginEntityResp {
name: e.entity_name.clone(),
display_name: e.entity_name,
table_name: e.table_name,
});
}
result
}
async fn find_plugin_entities(
plugin_id: Uuid,
tenant_id: Uuid,