feat(plugin): Schema 缓存 — moka LRU Cache 消除 resolve_entity_info 重复查库

- 添加 moka 0.12 依赖到 erp-plugin 和 erp-server
- 重写 state.rs: 新增 EntityInfo (含 generated_fields) 和 moka Cache
- AppState 新增 plugin_entity_cache 字段
- data_service.rs: 旧 resolve_entity_info 保留兼容,新增 resolve_entity_info_cached
This commit is contained in:
iven
2026-04-17 10:23:43 +08:00
parent 3b0b78c4cb
commit 091d517af6
6 changed files with 103 additions and 16 deletions

View File

@@ -23,3 +23,4 @@ utoipa = { workspace = true }
async-trait = { workspace = true }
sha2 = { workspace = true }
base64 = "0.22"
moka = { version = "0.12", features = ["sync"] }

View File

@@ -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<Vec<PluginField>> {
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<String, EntityInfo>,
) -> AppResult<EntityInfo> {
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<String> = 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(|| {

View File

@@ -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<String, EntityInfo>,
}
/// 缓存的实体信息
#[derive(Clone, Debug)]
pub struct EntityInfo {
pub table_name: String,
pub schema_json: serde_json::Value,
pub generated_fields: Vec<String>,
}
impl EntityInfo {
/// 从 schema_json 解析字段列表
pub fn fields(&self) -> AppResult<Vec<crate::manifest::PluginField>> {
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,
}
}
}

View File

@@ -30,3 +30,4 @@ erp-plugin.workspace = true
anyhow.workspace = true
uuid.workspace = true
chrono.workspace = true
moka = { version = "0.12", features = ["sync"] }

View File

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

View File

@@ -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<String, erp_plugin::state::EntityInfo>,
}
/// Allow handlers to extract `DatabaseConnection` directly from `State<AppState>`.
@@ -90,6 +92,7 @@ impl FromRef<AppState> 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(),
}
}
}