From 6a44cbecf304003bf72c2f9c7dfd3a61741250c1 Mon Sep 17 00:00:00 2001 From: iven Date: Fri, 17 Apr 2026 19:30:12 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20Q3=20N+1=20=E6=9F=A5=E8=AF=A2=E4=BC=98?= =?UTF-8?q?=E5=8C=96=20=E2=80=94=20user=5Fservice=20=E5=92=8C=20plugin=5Fs?= =?UTF-8?q?ervice?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - user_service::list() 循环内单查询改为 fetch_batch_user_role_resps 批量查询 - plugin_service::list() 循环内单查询改为 find_batch_plugin_entities 批量查询 - RoleResp 和 PluginEntityResp 添加 Clone derive --- crates/erp-auth/src/dto.rs | 2 +- crates/erp-auth/src/service/user_service.rs | 75 +++++++++++++++++++-- crates/erp-plugin/src/dto.rs | 2 +- crates/erp-plugin/src/service.rs | 55 +++++++++++++-- 4 files changed, 122 insertions(+), 12 deletions(-) diff --git a/crates/erp-auth/src/dto.rs b/crates/erp-auth/src/dto.rs index 0184fc0..c17574b 100644 --- a/crates/erp-auth/src/dto.rs +++ b/crates/erp-auth/src/dto.rs @@ -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, diff --git a/crates/erp-auth/src/service/user_service.rs b/crates/erp-auth/src/service/user_service.rs index 8d2f9b6..89ca344 100644 --- a/crates/erp-auth/src/service/user_service.rs +++ b/crates/erp-auth/src/service/user_service.rs @@ -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 = 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 = 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> { + if user_ids.is_empty() { + return HashMap::new(); + } + + // 1. 批量查询 user_role 关联 + let user_roles: Vec = 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 = user_roles.iter().map(|ur| ur.role_id).collect(); + + // 2. 批量查询角色 + let roles: Vec = 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 = + roles.iter().map(|r| (r.id, r)).collect(); + + // 3. 按 user_id 分组 + let mut result: HashMap> = 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, diff --git a/crates/erp-plugin/src/dto.rs b/crates/erp-plugin/src/dto.rs index fafbcd7..0749391 100644 --- a/crates/erp-plugin/src/dto.rs +++ b/crates/erp-plugin/src/dto.rs @@ -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, diff --git a/crates/erp-plugin/src/service.rs b/crates/erp-plugin/src/service.rs index 7fb7e07..e8abc48 100644 --- a/crates/erp-plugin/src/service.rs +++ b/crates/erp-plugin/src/service.rs @@ -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()); + + // 批量查询所有插件的 entities(N+1 → 2 固定查询) + let plugin_ids: Vec = 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 映射。 +async fn find_batch_plugin_entities( + plugin_ids: &[Uuid], + tenant_id: Uuid, + db: &sea_orm::DatabaseConnection, +) -> HashMap> { + 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> = 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,