diff --git a/crates/erp-plugin/Cargo.toml b/crates/erp-plugin/Cargo.toml index b035f24..3171a29 100644 --- a/crates/erp-plugin/Cargo.toml +++ b/crates/erp-plugin/Cargo.toml @@ -23,3 +23,4 @@ utoipa = { workspace = true } async-trait = { workspace = true } sha2 = { workspace = true } base64 = "0.22" +moka = { version = "0.12", features = ["sync"] } diff --git a/crates/erp-plugin/src/data_service.rs b/crates/erp-plugin/src/data_service.rs index 4c345a8..e9e41f9 100644 --- a/crates/erp-plugin/src/data_service.rs +++ b/crates/erp-plugin/src/data_service.rs @@ -5,29 +5,15 @@ use erp_core::error::{AppError, AppResult}; use erp_core::events::EventBus; use crate::data_dto::PluginDataResp; -use crate::dynamic_table::DynamicTableManager; +use crate::dynamic_table::{sanitize_identifier, DynamicTableManager}; use crate::entity::plugin; use crate::entity::plugin_entity; use crate::error::PluginError; use crate::manifest::PluginField; +use crate::state::EntityInfo; pub struct PluginDataService; -/// 插件实体信息(合并查询减少 DB 调用) -struct EntityInfo { - table_name: String, - schema_json: serde_json::Value, -} - -impl EntityInfo { - fn fields(&self) -> AppResult> { - let entity_def: crate::manifest::PluginEntity = - serde_json::from_value(self.schema_json.clone()) - .map_err(|e| AppError::Internal(format!("解析 entity schema 失败: {}", e)))?; - Ok(entity_def.fields) - } -} - impl PluginDataService { /// 创建插件数据 pub async fn create( @@ -391,6 +377,7 @@ pub async fn resolve_manifest_id( } /// 从 plugin_entities 表获取实体完整信息(带租户隔离) +/// 注意:此函数不填充 generated_fields,仅用于非 list 场景 async fn resolve_entity_info( plugin_id: Uuid, entity_name: &str, @@ -411,9 +398,61 @@ async fn resolve_entity_info( Ok(EntityInfo { table_name: entity.table_name, schema_json: entity.schema_json, + generated_fields: vec![], // 旧路径,不追踪 generated_fields }) } +/// 从缓存或数据库获取实体信息(带 generated_fields 解析) +pub async fn resolve_entity_info_cached( + plugin_id: Uuid, + entity_name: &str, + tenant_id: Uuid, + db: &sea_orm::DatabaseConnection, + cache: &moka::sync::Cache, +) -> AppResult { + let cache_key = format!("{}:{}:{}", plugin_id, entity_name, tenant_id); + if let Some(info) = cache.get(&cache_key) { + return Ok(info); + } + + let entity = plugin_entity::Entity::find() + .filter(plugin_entity::Column::PluginId.eq(plugin_id)) + .filter(plugin_entity::Column::TenantId.eq(tenant_id)) + .filter(plugin_entity::Column::EntityName.eq(entity_name)) + .filter(plugin_entity::Column::DeletedAt.is_null()) + .one(db) + .await? + .ok_or_else(|| { + AppError::NotFound(format!("插件实体 {}/{} 不存在", plugin_id, entity_name)) + })?; + + // 解析 generated_fields + let entity_def: crate::manifest::PluginEntity = + serde_json::from_value(entity.schema_json.clone()) + .map_err(|e| AppError::Internal(format!("解析 entity schema 失败: {}", e)))?; + let generated_fields: Vec = entity_def + .fields + .iter() + .filter(|f| f.field_type.supports_generated_column()) + .filter(|f| { + f.unique + || f.sortable == Some(true) + || f.filterable == Some(true) + || (f.required && (f.sortable == Some(true) || f.filterable == Some(true))) + }) + .map(|f| sanitize_identifier(&f.name)) + .collect(); + + let info = EntityInfo { + table_name: entity.table_name, + schema_json: entity.schema_json, + generated_fields, + }; + + cache.insert(cache_key, info.clone()); + Ok(info) +} + /// 校验数据:检查 required 字段 fn validate_data(data: &serde_json::Value, fields: &[PluginField]) -> AppResult<()> { let obj = data.as_object().ok_or_else(|| { diff --git a/crates/erp-plugin/src/state.rs b/crates/erp-plugin/src/state.rs index 6e1c420..7cb4df9 100644 --- a/crates/erp-plugin/src/state.rs +++ b/crates/erp-plugin/src/state.rs @@ -1,5 +1,9 @@ +use std::time::Duration; + +use moka::sync::Cache; use sea_orm::DatabaseConnection; +use erp_core::error::{AppError, AppResult}; use erp_core::events::EventBus; use crate::engine::PluginEngine; @@ -10,4 +14,39 @@ pub struct PluginState { pub db: DatabaseConnection, pub event_bus: EventBus, pub engine: PluginEngine, + /// Schema 缓存 — key: "{plugin_id}:{entity_name}:{tenant_id}" + pub entity_cache: Cache, +} + +/// 缓存的实体信息 +#[derive(Clone, Debug)] +pub struct EntityInfo { + pub table_name: String, + pub schema_json: serde_json::Value, + pub generated_fields: Vec, +} + +impl EntityInfo { + /// 从 schema_json 解析字段列表 + pub fn fields(&self) -> AppResult> { + let entity_def: crate::manifest::PluginEntity = + serde_json::from_value(self.schema_json.clone()) + .map_err(|e| AppError::Internal(format!("解析 entity schema 失败: {}", e)))?; + Ok(entity_def.fields) + } +} + +impl PluginState { + pub fn new(db: DatabaseConnection, event_bus: EventBus, engine: PluginEngine) -> Self { + let entity_cache = Cache::builder() + .max_capacity(1000) + .time_to_idle(Duration::from_secs(300)) + .build(); + Self { + db, + event_bus, + engine, + entity_cache, + } + } } diff --git a/crates/erp-server/Cargo.toml b/crates/erp-server/Cargo.toml index 5185547..b458780 100644 --- a/crates/erp-server/Cargo.toml +++ b/crates/erp-server/Cargo.toml @@ -30,3 +30,4 @@ erp-plugin.workspace = true anyhow.workspace = true uuid.workspace = true chrono.workspace = true +moka = { version = "0.12", features = ["sync"] } diff --git a/crates/erp-server/src/main.rs b/crates/erp-server/src/main.rs index 22d1414..5e9e8f9 100644 --- a/crates/erp-server/src/main.rs +++ b/crates/erp-server/src/main.rs @@ -371,6 +371,10 @@ async fn main() -> anyhow::Result<()> { redis: redis_client.clone(), default_tenant_id, plugin_engine, + plugin_entity_cache: moka::sync::Cache::builder() + .max_capacity(1000) + .time_to_idle(std::time::Duration::from_secs(300)) + .build(), }; // --- Build the router --- diff --git a/crates/erp-server/src/state.rs b/crates/erp-server/src/state.rs index f242042..4352ae7 100644 --- a/crates/erp-server/src/state.rs +++ b/crates/erp-server/src/state.rs @@ -18,6 +18,8 @@ pub struct AppState { pub default_tenant_id: uuid::Uuid, /// 插件引擎 pub plugin_engine: erp_plugin::engine::PluginEngine, + /// 插件实体缓存 + pub plugin_entity_cache: moka::sync::Cache, } /// Allow handlers to extract `DatabaseConnection` directly from `State`. @@ -90,6 +92,7 @@ impl FromRef for erp_plugin::state::PluginState { db: state.db.clone(), event_bus: state.event_bus.clone(), engine: state.plugin_engine.clone(), + entity_cache: state.plugin_entity_cache.clone(), } } }