use chrono::Utc; use sea_orm::{ActiveModelTrait, ColumnTrait, ConnectionTrait, EntityTrait, PaginatorTrait, QueryFilter, Set}; use std::collections::HashMap; use uuid::Uuid; use sha2::{Sha256, Digest}; use erp_core::error::AppResult; use crate::dto::{ PluginEntityResp, PluginHealthResp, PluginPermissionResp, PluginResp, }; use crate::dynamic_table::DynamicTableManager; use crate::engine::PluginEngine; use crate::entity::{plugin, plugin_entity, plugin_event_subscription}; use crate::error::PluginError; use crate::manifest::{parse_manifest, PluginManifest}; pub struct PluginService; impl PluginService { /// 上传插件: 解析 manifest + 存储 wasm_binary + status=uploaded pub async fn upload( tenant_id: Uuid, operator_id: Uuid, wasm_binary: Vec, manifest_toml: &str, db: &sea_orm::DatabaseConnection, ) -> AppResult { // 解析 manifest let manifest = parse_manifest(manifest_toml)?; // 安全扫描 let validation = crate::plugin_validator::validate_plugin_security(&manifest, wasm_binary.len())?; if !validation.valid { return Err(PluginError::ValidationError(format!( "插件安全校验失败: {}", validation.errors.join("; ") )).into()); } // 计算 WASM hash let mut hasher = Sha256::new(); hasher.update(&wasm_binary); let wasm_hash = format!("{:x}", hasher.finalize()); let now = Utc::now(); let plugin_id = Uuid::now_v7(); // 序列化 manifest 为 JSON let manifest_json = serde_json::to_value(&manifest).map_err(|e| PluginError::InvalidManifest(e.to_string()))?; let model = plugin::ActiveModel { id: Set(plugin_id), tenant_id: Set(tenant_id), name: Set(manifest.metadata.name.clone()), plugin_version: Set(manifest.metadata.version.clone()), description: Set(if manifest.metadata.description.is_empty() { None } else { Some(manifest.metadata.description.clone()) }), author: Set(if manifest.metadata.author.is_empty() { None } else { Some(manifest.metadata.author.clone()) }), status: Set("uploaded".to_string()), manifest_json: Set(manifest_json), wasm_binary: Set(wasm_binary), wasm_hash: Set(wasm_hash), config_json: Set(serde_json::json!({})), error_message: Set(None), installed_at: Set(None), enabled_at: Set(None), created_at: Set(now), updated_at: Set(now), created_by: Set(Some(operator_id)), updated_by: Set(Some(operator_id)), deleted_at: Set(None), version: Set(1), }; let model = model.insert(db).await?; Ok(plugin_model_to_resp(&model, &manifest, vec![])) } /// 安装插件: 创建动态表 + 注册 entity 记录 + 注册事件订阅 + 注册权限 + status=installed pub async fn install( plugin_id: Uuid, tenant_id: Uuid, operator_id: Uuid, db: &sea_orm::DatabaseConnection, engine: &PluginEngine, ) -> AppResult { let model = find_plugin(plugin_id, tenant_id, db).await?; validate_status(&model.status, "uploaded")?; let manifest: PluginManifest = serde_json::from_value(model.manifest_json.clone()) .map_err(|e| PluginError::InvalidManifest(e.to_string()))?; let now = Utc::now(); // 创建动态表 + 注册 entity 记录 let mut entity_resps = Vec::new(); if let Some(schema) = &manifest.schema { for (i, entity_def) in schema.entities.iter().enumerate() { let table_name = DynamicTableManager::table_name(&manifest.metadata.id, &entity_def.name); tracing::info!(step = i, entity = %entity_def.name, table = %table_name, "Creating dynamic table"); // 创建动态表 DynamicTableManager::create_table(db, &manifest.metadata.id, entity_def).await .map_err(|e| { tracing::error!(entity = %entity_def.name, table = %table_name, error = %e, "Failed to create dynamic table"); e })?; // 注册 entity 记录 let entity_id = Uuid::now_v7(); let entity_model = plugin_entity::ActiveModel { id: Set(entity_id), tenant_id: Set(tenant_id), plugin_id: Set(plugin_id), entity_name: Set(entity_def.name.clone()), table_name: Set(table_name.clone()), schema_json: Set(serde_json::to_value(entity_def).unwrap_or_default()), manifest_id: Set(manifest.metadata.id.clone()), is_public: Set(entity_def.is_public.unwrap_or(false)), created_at: Set(now), updated_at: Set(now), created_by: Set(Some(operator_id)), updated_by: Set(Some(operator_id)), deleted_at: Set(None), version: Set(1), }; entity_model.insert(db).await?; entity_resps.push(PluginEntityResp { name: entity_def.name.clone(), display_name: entity_def.display_name.clone(), table_name, }); } } // 注册事件订阅 if let Some(events) = &manifest.events { for pattern in &events.subscribe { let sub_id = Uuid::now_v7(); let sub_model = plugin_event_subscription::ActiveModel { id: Set(sub_id), plugin_id: Set(plugin_id), event_pattern: Set(pattern.clone()), created_at: Set(now), }; sub_model.insert(db).await?; } } // 注册插件声明的权限到 permissions 表 tracing::info!("Registering plugin permissions"); if let Some(perms) = &manifest.permissions { register_plugin_permissions( db, tenant_id, operator_id, &manifest.metadata.id, perms, &now, ) .await .map_err(|e| { tracing::error!(error = %e, "Failed to register permissions"); e })?; } // 将插件权限自动分配给 admin 角色 tracing::info!("Granting plugin permissions to admin role"); grant_permissions_to_admin(db, tenant_id, &manifest.metadata.id).await?; // 加载到内存 tracing::info!(manifest_id = %manifest.metadata.id, "Loading plugin into engine"); engine .load( &manifest.metadata.id, &model.wasm_binary, manifest.clone(), ) .await?; // 更新状态 let mut active: plugin::ActiveModel = model.into(); active.status = Set("installed".to_string()); active.installed_at = Set(Some(now)); active.updated_at = Set(now); active.updated_by = Set(Some(operator_id)); let model = active.update(db).await?; Ok(plugin_model_to_resp(&model, &manifest, entity_resps)) } /// 启用插件: engine.initialize + start_event_listener + status=running pub async fn enable( plugin_id: Uuid, tenant_id: Uuid, operator_id: Uuid, db: &sea_orm::DatabaseConnection, engine: &PluginEngine, ) -> AppResult { let model = find_plugin(plugin_id, tenant_id, db).await?; validate_status_any(&model.status, &["installed", "disabled"])?; let manifest: PluginManifest = serde_json::from_value(model.manifest_json.clone()) .map_err(|e| PluginError::InvalidManifest(e.to_string()))?; let plugin_manifest_id = &manifest.metadata.id; // 确保插件权限已分配给 admin 角色(幂等操作) grant_permissions_to_admin(db, tenant_id, plugin_manifest_id).await?; // 如果之前是 disabled 状态,需要先卸载再重新加载到内存 // (disable 只改内存状态但不从 DashMap 移除) if model.status == "disabled" { engine.unload(plugin_manifest_id).await.ok(); engine .load(plugin_manifest_id, &model.wasm_binary, manifest.clone()) .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)) } }; // 启动事件监听(非致命) 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(); active.status = Set("running".to_string()); active.enabled_at = Set(Some(now)); active.updated_at = Set(now); active.updated_by = Set(Some(operator_id)); active.error_message = Set(init_error); let model = active.update(db).await?; let entity_resps = find_plugin_entities(plugin_id, tenant_id, db).await?; Ok(plugin_model_to_resp(&model, &manifest, entity_resps)) } /// 禁用插件: engine.disable + cancel 事件订阅 + status=disabled pub async fn disable( plugin_id: Uuid, tenant_id: Uuid, operator_id: Uuid, db: &sea_orm::DatabaseConnection, engine: &PluginEngine, ) -> AppResult { let model = find_plugin(plugin_id, tenant_id, db).await?; validate_status_any(&model.status, &["running", "enabled"])?; let manifest: PluginManifest = serde_json::from_value(model.manifest_json.clone()) .map_err(|e| PluginError::InvalidManifest(e.to_string()))?; // 禁用引擎 engine.disable(&manifest.metadata.id).await?; let now = Utc::now(); let mut active: plugin::ActiveModel = model.into(); active.status = Set("disabled".to_string()); active.updated_at = Set(now); active.updated_by = Set(Some(operator_id)); let model = active.update(db).await?; let entity_resps = find_plugin_entities(plugin_id, tenant_id, db).await?; Ok(plugin_model_to_resp(&model, &manifest, entity_resps)) } /// 卸载插件: unload + 有条件地 drop 动态表 + 清理权限 + status=uninstalled pub async fn uninstall( plugin_id: Uuid, tenant_id: Uuid, operator_id: Uuid, db: &sea_orm::DatabaseConnection, engine: &PluginEngine, ) -> AppResult { let model = find_plugin(plugin_id, tenant_id, db).await?; validate_status_any(&model.status, &["installed", "disabled"])?; let manifest: PluginManifest = serde_json::from_value(model.manifest_json.clone()) .map_err(|e| PluginError::InvalidManifest(e.to_string()))?; // 卸载(如果 disabled 状态,engine 可能仍在内存中) engine.unload(&manifest.metadata.id).await.ok(); // 软删除当前租户的 entity 记录 let now = Utc::now(); let tenant_entities = plugin_entity::Entity::find() .filter(plugin_entity::Column::PluginId.eq(plugin_id)) .filter(plugin_entity::Column::TenantId.eq(tenant_id)) .filter(plugin_entity::Column::DeletedAt.is_null()) .all(db) .await?; for entity in &tenant_entities { let mut active: plugin_entity::ActiveModel = entity.clone().into(); active.deleted_at = Set(Some(now)); active.updated_at = Set(now); active.updated_by = Set(Some(operator_id)); active.update(db).await?; } // 仅当没有其他租户的活跃 entity 记录引用相同的 table_name 时才 drop 表 if let Some(schema) = &manifest.schema { for entity_def in &schema.entities { let table_name = DynamicTableManager::table_name(&manifest.metadata.id, &entity_def.name); // 检查是否还有其他租户的活跃 entity 记录引用此表 let other_tenants_count = plugin_entity::Entity::find() .filter(plugin_entity::Column::TableName.eq(&table_name)) .filter(plugin_entity::Column::TenantId.ne(tenant_id)) .filter(plugin_entity::Column::DeletedAt.is_null()) .count(db) .await?; if other_tenants_count == 0 { // 没有其他租户使用,安全删除 DynamicTableManager::drop_table(db, &manifest.metadata.id, &entity_def.name) .await .ok(); } } } // 清理此插件注册的权限 unregister_plugin_permissions(db, tenant_id, &manifest.metadata.id).await?; let mut active: plugin::ActiveModel = model.into(); active.status = Set("uninstalled".to_string()); active.updated_at = Set(now); active.updated_by = Set(Some(operator_id)); let model = active.update(db).await?; Ok(plugin_model_to_resp(&model, &manifest, vec![])) } /// 列表查询 pub async fn list( tenant_id: Uuid, page: u64, page_size: u64, status: Option<&str>, search: Option<&str>, db: &sea_orm::DatabaseConnection, ) -> AppResult<(Vec, u64)> { let mut query = plugin::Entity::find() .filter(plugin::Column::TenantId.eq(tenant_id)) .filter(plugin::Column::DeletedAt.is_null()); if let Some(s) = status { query = query.filter(plugin::Column::Status.eq(s)); } if let Some(q) = search { query = query.filter( plugin::Column::Name.contains(q) .or(plugin::Column::Description.contains(q)), ); } let paginator = query .clone() .paginate(db, page_size); let total = paginator.num_items().await?; let models = paginator .fetch_page(page.saturating_sub(1)) .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(|_| { PluginManifest { metadata: crate::manifest::PluginMetadata { id: String::new(), name: String::new(), version: String::new(), description: String::new(), author: String::new(), min_platform_version: None, dependencies: vec![], }, schema: None, events: None, ui: None, permissions: None, settings: None, numbering: None, templates: None, trigger_events: None, } }); let entities = entities_map.get(&model.id).cloned().unwrap_or_default(); resps.push(plugin_model_to_resp(&model, &manifest, entities)); } Ok((resps, total)) } /// 按 ID 获取详情 pub async fn get_by_id( plugin_id: Uuid, tenant_id: Uuid, db: &sea_orm::DatabaseConnection, ) -> AppResult { let model = find_plugin(plugin_id, tenant_id, db).await?; let manifest: PluginManifest = serde_json::from_value(model.manifest_json.clone()) .map_err(|e| PluginError::InvalidManifest(e.to_string()))?; let entities = find_plugin_entities(plugin_id, tenant_id, db).await?; Ok(plugin_model_to_resp(&model, &manifest, entities)) } /// 更新配置 pub async fn update_config( plugin_id: Uuid, tenant_id: Uuid, operator_id: Uuid, config: serde_json::Value, expected_version: i32, db: &sea_orm::DatabaseConnection, ) -> AppResult { let model = find_plugin(plugin_id, tenant_id, db).await?; erp_core::error::check_version(expected_version, model.version)?; // 校验配置值是否符合 manifest settings 声明 let manifest: PluginManifest = serde_json::from_value(model.manifest_json.clone()) .map_err(|e| PluginError::InvalidManifest(e.to_string()))?; if let Some(settings) = &manifest.settings { validate_plugin_settings(config.as_object().ok_or_else(|| { PluginError::ValidationError("config 必须是 JSON 对象".to_string()) })?, &settings.fields)?; } let now = Utc::now(); let mut active: plugin::ActiveModel = model.into(); active.config_json = Set(config); active.updated_at = Set(now); active.updated_by = Set(Some(operator_id)); let model = active.update(db).await?; let entities = find_plugin_entities(plugin_id, tenant_id, db).await.unwrap_or_default(); Ok(plugin_model_to_resp(&model, &manifest, entities)) } /// 健康检查 pub async fn health_check( plugin_id: Uuid, tenant_id: Uuid, db: &sea_orm::DatabaseConnection, engine: &PluginEngine, ) -> AppResult { let model = find_plugin(plugin_id, tenant_id, db).await?; let manifest: PluginManifest = serde_json::from_value(model.manifest_json.clone()) .map_err(|e| PluginError::InvalidManifest(e.to_string()))?; let details = engine.health_check(&manifest.metadata.id).await?; Ok(PluginHealthResp { plugin_id, status: details .get("status") .and_then(|v| v.as_str()) .unwrap_or("unknown") .to_string(), details, }) } /// 获取插件 Schema pub async fn get_schema( plugin_id: Uuid, tenant_id: Uuid, db: &sea_orm::DatabaseConnection, ) -> AppResult { let model = find_plugin(plugin_id, tenant_id, db).await?; let manifest: PluginManifest = serde_json::from_value(model.manifest_json.clone()) .map_err(|e| PluginError::InvalidManifest(e.to_string()))?; // 构建 schema 响应:entities + ui 页面配置 + settings + numbering + trigger_events let mut result = serde_json::Map::new(); if let Some(schema) = &manifest.schema { result.insert( "entities".to_string(), serde_json::to_value(&schema.entities).unwrap_or_default(), ); } if let Some(ui) = &manifest.ui { result.insert( "ui".to_string(), serde_json::to_value(ui).unwrap_or_default(), ); } if let Some(settings) = &manifest.settings { result.insert( "settings".to_string(), serde_json::to_value(settings).unwrap_or_default(), ); } if let Some(numbering) = &manifest.numbering { result.insert( "numbering".to_string(), serde_json::to_value(numbering).unwrap_or_default(), ); } if let Some(triggers) = &manifest.trigger_events { result.insert( "trigger_events".to_string(), serde_json::to_value(triggers).unwrap_or_default(), ); } Ok(serde_json::Value::Object(result)) } /// 清除插件记录(软删除,仅限已卸载状态) pub async fn purge( plugin_id: Uuid, tenant_id: Uuid, operator_id: Uuid, db: &sea_orm::DatabaseConnection, ) -> AppResult<()> { let model = find_plugin(plugin_id, tenant_id, db).await?; validate_status_any(&model.status, &["uninstalled", "uploaded"])?; let now = Utc::now(); let mut active: plugin::ActiveModel = model.into(); active.deleted_at = Set(Some(now)); active.updated_at = Set(now); active.updated_by = Set(Some(operator_id)); active.update(db).await?; Ok(()) } /// 热更新插件 — 上传新版本 WASM + manifest,对比 schema 变更,执行增量 DDL /// /// 流程: /// 1. 解析新 manifest /// 2. 获取当前插件信息 /// 3. 对比 schema 变更,为新增实体创建表 /// 4. 卸载旧 WASM,加载新 WASM /// 5. 更新数据库记录 /// 6. 失败时保持旧版本继续运行(回滚) pub async fn upgrade( plugin_id: Uuid, tenant_id: Uuid, operator_id: Uuid, new_wasm: Vec, new_manifest_toml: &str, db: &sea_orm::DatabaseConnection, engine: &PluginEngine, ) -> AppResult { let new_manifest = parse_manifest(new_manifest_toml)?; let model = find_plugin(plugin_id, tenant_id, db).await?; let old_manifest: PluginManifest = serde_json::from_value(model.manifest_json.clone()) .map_err(|e| PluginError::InvalidManifest(e.to_string()))?; let old_version = old_manifest.metadata.version.clone(); let new_version = new_manifest.metadata.version.clone(); if old_manifest.metadata.id != new_manifest.metadata.id { return Err(PluginError::InvalidManifest( format!("插件 ID 不匹配: 旧={}, 新={}", old_manifest.metadata.id, new_manifest.metadata.id) ).into()); } let plugin_manifest_id = &new_manifest.metadata.id; // 对比 schema — 为新增实体创建动态表 + 已有实体字段演进 if let Some(new_schema) = &new_manifest.schema { let old_schema = old_manifest.schema.as_ref(); for entity in &new_schema.entities { let old_entity = old_schema .and_then(|s| s.entities.iter().find(|e| e.name == entity.name)); match old_entity { None => { tracing::info!(entity = %entity.name, "创建新增实体表"); DynamicTableManager::create_table(db, plugin_manifest_id, entity).await?; } Some(old) => { let diff = DynamicTableManager::diff_entity_fields(old, entity); if !diff.new_filterable.is_empty() || !diff.new_searchable.is_empty() { tracing::info!( entity = %entity.name, new_cols = diff.new_filterable.len(), new_search = diff.new_searchable.len(), "Schema 演进:新增 Generated Column" ); DynamicTableManager::alter_add_generated_columns( db, plugin_manifest_id, entity, &diff ).await?; } } } } } // 先加载新版本到临时 key,确保成功后再替换旧版本(原子回滚) let temp_id = format!("{}__upgrade_{}", plugin_manifest_id, Uuid::now_v7()); engine .load(&temp_id, &new_wasm, new_manifest.clone()) .await .map_err(|e| { tracing::error!(error = %e, "新版本 WASM 加载失败,旧版本仍在运行"); e })?; // 新版本加载成功,卸载旧版本并重命名新版本为正式 key engine.unload(plugin_manifest_id).await.ok(); engine.rename_plugin(&temp_id, plugin_manifest_id).await?; // 更新数据库记录 let wasm_hash = { let mut hasher = Sha256::new(); hasher.update(&new_wasm); format!("{:x}", hasher.finalize()) }; let now = Utc::now(); let mut active: plugin::ActiveModel = model.into(); active.wasm_binary = Set(new_wasm); active.wasm_hash = Set(wasm_hash); active.manifest_json = Set(serde_json::to_value(&new_manifest) .map_err(|e| PluginError::InvalidManifest(e.to_string()))?); active.plugin_version = Set(new_version.clone()); active.updated_at = Set(now); active.updated_by = Set(Some(operator_id)); active.version = Set(active.version.unwrap() + 1); let updated = active .update(db) .await .map_err(|e| PluginError::DatabaseError(e.to_string()))?; // 更新 plugin_entities 表中的 schema_json if let Some(schema) = &new_manifest.schema { for entity in &schema.entities { let entity_model = 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 .map_err(|e| PluginError::DatabaseError(e.to_string()))?; if let Some(em) = entity_model { let mut active: plugin_entity::ActiveModel = em.into(); active.schema_json = Set(serde_json::to_value(entity) .map_err(|e| PluginError::InvalidManifest(e.to_string()))?); active.updated_at = Set(now); active.updated_by = Set(Some(operator_id)); active.update(db).await.map_err(|e| PluginError::DatabaseError(e.to_string()))?; } } } tracing::info!( plugin_id = %plugin_id, old_version = %old_version, new_version = %new_version, "插件热更新成功" ); let entities = find_plugin_entities(plugin_id, tenant_id, db).await?; Ok(plugin_model_to_resp(&updated, &new_manifest, entities)) } } // ---- 内部辅助 ---- fn find_plugin( plugin_id: Uuid, tenant_id: Uuid, db: &sea_orm::DatabaseConnection, ) -> impl std::future::Future> + Send { async move { plugin::Entity::find_by_id(plugin_id) .one(db) .await? .filter(|m| m.tenant_id == tenant_id && m.deleted_at.is_none()) .ok_or_else(|| { erp_core::error::AppError::NotFound(format!("插件 {} 不存在", plugin_id)) }) } } /// 公开的插件查询 — 供 handler 使用 pub async fn find_plugin_model( plugin_id: Uuid, tenant_id: Uuid, db: &sea_orm::DatabaseConnection, ) -> AppResult { find_plugin(plugin_id, tenant_id, db).await } /// 批量查询多插件的 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, db: &sea_orm::DatabaseConnection, ) -> AppResult> { let entities = plugin_entity::Entity::find() .filter(plugin_entity::Column::PluginId.eq(plugin_id)) .filter(plugin_entity::Column::TenantId.eq(tenant_id)) .filter(plugin_entity::Column::DeletedAt.is_null()) .all(db) .await?; Ok(entities .into_iter() .map(|e| PluginEntityResp { name: e.entity_name.clone(), display_name: e.entity_name, table_name: e.table_name, }) .collect()) } fn validate_status(actual: &str, expected: &str) -> AppResult<()> { if actual != expected { return Err(PluginError::InvalidState { expected: expected.to_string(), actual: actual.to_string(), } .into()); } Ok(()) } fn validate_status_any(actual: &str, expected: &[&str]) -> AppResult<()> { if !expected.contains(&actual) { return Err(PluginError::InvalidState { expected: expected.join(" 或 "), actual: actual.to_string(), } .into()); } Ok(()) } /// 校验配置值是否符合 manifest settings 声明 fn validate_plugin_settings( config: &serde_json::Map, fields: &[crate::manifest::PluginSettingField], ) -> AppResult<()> { use crate::manifest::PluginSettingType; for field in fields { let value = config.get(&field.name); // 必填校验 if field.required { match value { None | Some(serde_json::Value::Null) => { return Err(PluginError::ValidationError(format!( "配置项 '{}' ({}) 为必填", field.name, field.display_name )) .into()); } Some(serde_json::Value::String(s)) if s.is_empty() => { return Err(PluginError::ValidationError(format!( "配置项 '{}' ({}) 不能为空", field.name, field.display_name )) .into()); } _ => {} } } // 类型校验 if let Some(val) = value { if !val.is_null() { let type_ok = match field.field_type { PluginSettingType::Text => val.is_string(), PluginSettingType::Number => val.is_number(), PluginSettingType::Boolean => val.is_boolean(), PluginSettingType::Select => val.is_string(), PluginSettingType::Multiselect => val.is_array(), PluginSettingType::Color => val.is_string(), PluginSettingType::Date => val.is_string(), PluginSettingType::Datetime => val.is_string(), PluginSettingType::Json => true, }; if !type_ok { return Err(PluginError::ValidationError(format!( "配置项 '{}' 类型错误,期望 {:?}", field.name, field.field_type )) .into()); } // 数值范围校验 if let Some((min, max)) = field.range { if let Some(n) = val.as_f64() { if n < min || n > max { return Err(PluginError::ValidationError(format!( "配置项 '{}' ({}) 的值 {} 超出范围 [{}, {}]", field.name, field.display_name, n, min, max )) .into()); } } } } } } Ok(()) } fn plugin_model_to_resp( model: &plugin::Model, manifest: &PluginManifest, entities: Vec, ) -> PluginResp { let permissions = manifest.permissions.as_ref().map(|perms| { perms .iter() .map(|p| PluginPermissionResp { code: p.code.clone(), name: p.name.clone(), description: p.description.clone(), }) .collect() }); PluginResp { id: model.id, name: model.name.clone(), version: model.plugin_version.clone(), description: model.description.clone(), author: model.author.clone(), status: model.status.clone(), config: model.config_json.clone(), installed_at: model.installed_at, enabled_at: model.enabled_at, entities, permissions, record_version: model.version, } } /// 将插件声明的权限注册到 permissions 表。 /// /// 使用 raw SQL 避免依赖 erp-auth 的 entity 类型。 /// 权限码格式:`{plugin_manifest_id}.{code}`(如 `erp-crm.customer.list`)。 /// 使用 `ON CONFLICT DO NOTHING` 保证幂等。 async fn register_plugin_permissions( db: &sea_orm::DatabaseConnection, tenant_id: Uuid, operator_id: Uuid, plugin_manifest_id: &str, perms: &[crate::manifest::PluginPermission], now: &chrono::DateTime, ) -> AppResult<()> { for perm in perms { let full_code = format!("{}.{}", plugin_manifest_id, perm.code); let resource = plugin_manifest_id.to_string(); let action = perm.code.clone(); let description: Option = if perm.description.is_empty() { None } else { Some(perm.description.clone()) }; let sql = r#" INSERT INTO permissions (id, tenant_id, code, name, resource, action, description, created_at, updated_at, created_by, updated_by, deleted_at, version) VALUES (gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $7, $8, $8, NULL, 1) ON CONFLICT (tenant_id, code) WHERE deleted_at IS NULL DO NOTHING "#; db.execute(sea_orm::Statement::from_sql_and_values( sea_orm::DatabaseBackend::Postgres, sql, vec![ sea_orm::Value::from(tenant_id), sea_orm::Value::from(full_code.clone()), sea_orm::Value::from(perm.name.clone()), sea_orm::Value::from(resource), sea_orm::Value::from(action), sea_orm::Value::from(description), sea_orm::Value::from(now.clone()), sea_orm::Value::from(operator_id), ], )) .await .map_err(|e| { tracing::error!( plugin = plugin_manifest_id, permission = %full_code, error = %e, "注册插件权限失败" ); PluginError::DatabaseError(format!("注册插件权限 {} 失败: {}", full_code, e)) })?; } tracing::info!( plugin = plugin_manifest_id, count = perms.len(), tenant_id = %tenant_id, "插件权限注册完成" ); Ok(()) } /// 将插件的所有权限分配给 admin 角色。 /// /// 使用 raw SQL 按 manifest_id 前缀匹配权限,INSERT 到 role_permissions。 /// ON CONFLICT DO NOTHING 保证幂等。 pub async fn grant_permissions_to_admin( db: &sea_orm::DatabaseConnection, tenant_id: Uuid, plugin_manifest_id: &str, ) -> AppResult<()> { let prefix = format!("{}.%", plugin_manifest_id); let sql = r#" INSERT INTO role_permissions (tenant_id, role_id, permission_id, data_scope, created_at, updated_at, created_by, updated_by, deleted_at, version) SELECT p.tenant_id, r.id, p.id, 'all', NOW(), NOW(), r.id, r.id, NULL, 1 FROM permissions p CROSS JOIN roles r WHERE p.tenant_id = $1 AND r.tenant_id = $1 AND r.code = 'admin' AND r.deleted_at IS NULL AND p.code LIKE $2 AND p.deleted_at IS NULL AND NOT EXISTS ( SELECT 1 FROM role_permissions rp WHERE rp.permission_id = p.id AND rp.role_id = r.id AND rp.deleted_at IS NULL ) ON CONFLICT (role_id, permission_id) DO NOTHING "#; let result = db .execute(sea_orm::Statement::from_sql_and_values( sea_orm::DatabaseBackend::Postgres, sql, vec![ sea_orm::Value::from(tenant_id), sea_orm::Value::from(prefix.clone()), ], )) .await .map_err(|e| { tracing::error!( plugin = plugin_manifest_id, error = %e, "分配插件权限给 admin 角色失败" ); PluginError::DatabaseError(format!( "分配插件权限给 admin 角色失败: {}", e )) })?; let rows = result.rows_affected(); tracing::info!( plugin = plugin_manifest_id, rows_affected = rows, tenant_id = %tenant_id, "插件权限已分配给 admin 角色" ); Ok(()) } /// 清理插件注册的权限(软删除)。 /// /// 使用 raw SQL 按前缀匹配清理:`{plugin_manifest_id}.%`。 /// 同时清理 role_permissions 中对这些权限的关联。 async fn unregister_plugin_permissions( db: &sea_orm::DatabaseConnection, tenant_id: Uuid, plugin_manifest_id: &str, ) -> AppResult<()> { let prefix = format!("{}.%", plugin_manifest_id); let now = chrono::Utc::now(); // 先软删除 role_permissions 中的关联 let rp_sql = r#" UPDATE role_permissions SET deleted_at = $1, updated_at = $1 WHERE permission_id IN ( SELECT id FROM permissions WHERE tenant_id = $2 AND code LIKE $3 AND deleted_at IS NULL ) AND deleted_at IS NULL "#; db.execute(sea_orm::Statement::from_sql_and_values( sea_orm::DatabaseBackend::Postgres, rp_sql, vec![ sea_orm::Value::from(now.clone()), sea_orm::Value::from(tenant_id), sea_orm::Value::from(prefix.clone()), ], )) .await .map_err(|e| { tracing::error!( plugin = plugin_manifest_id, error = %e, "清理插件权限角色关联失败" ); PluginError::DatabaseError(format!("清理插件权限角色关联失败: {}", e)) })?; // 再软删除 permissions let perm_sql = r#" UPDATE permissions SET deleted_at = $1, updated_at = $1 WHERE tenant_id = $2 AND code LIKE $3 AND deleted_at IS NULL "#; db.execute(sea_orm::Statement::from_sql_and_values( sea_orm::DatabaseBackend::Postgres, perm_sql, vec![ sea_orm::Value::from(now), sea_orm::Value::from(tenant_id), sea_orm::Value::from(prefix), ], )) .await .map_err(|e| { tracing::error!( plugin = plugin_manifest_id, error = %e, "清理插件权限失败" ); PluginError::DatabaseError(format!("清理插件权限失败: {}", e)) })?; tracing::info!( plugin = plugin_manifest_id, tenant_id = %tenant_id, "插件权限清理完成" ); Ok(()) }