From 3483395f5eddfac9e3b854d0cc722466b4b6769b Mon Sep 17 00:00:00 2001 From: iven Date: Thu, 16 Apr 2026 23:42:40 +0800 Subject: [PATCH] =?UTF-8?q?fix(plugin):=20=E4=BF=AE=E5=A4=8D=E6=8F=92?= =?UTF-8?q?=E4=BB=B6=20schema=20API=E3=80=81=E5=8A=A8=E6=80=81=E8=A1=A8=20?= =?UTF-8?q?JSONB=20=E5=92=8C=20SQL=20=E6=B3=A8=E5=85=A5=E9=98=B2=E6=8A=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - get_schema 端点同时返回 entities 和 ui 页面配置,修复前端无法生成动态菜单的问题 - 动态表 INSERT/UPDATE 添加 ::jsonb 类型转换,修复 PostgreSQL 类型推断错误 - JSONB 索引创建改为非致命(warn 跳过),避免索引冲突阻断安装流程 - 权限注册/注销改用参数化查询,消除 SQL 注入风险 - DDL 语句改用 execute_unprepared,避免不必要的安全检查开销 - clear_plugin 支持已上传状态的清理 - 添加关键步骤 tracing 日志便于排查安装问题 --- crates/erp-plugin-crm/plugin.toml | 74 +++++------ crates/erp-plugin/src/data_service.rs | 118 ++++++++++-------- crates/erp-plugin/src/dynamic_table.rs | 56 +++++---- crates/erp-plugin/src/handler/data_handler.rs | 30 +++-- .../erp-plugin/src/handler/plugin_handler.rs | 6 +- crates/erp-plugin/src/service.rs | 117 ++++++++++------- 6 files changed, 225 insertions(+), 176 deletions(-) diff --git a/crates/erp-plugin-crm/plugin.toml b/crates/erp-plugin-crm/plugin.toml index 77c44a0..6d7b4f1 100644 --- a/crates/erp-plugin-crm/plugin.toml +++ b/crates/erp-plugin-crm/plugin.toml @@ -54,7 +54,7 @@ display_name = "客户" [[schema.entities.fields]] name = "code" - field_type = "String" + field_type = "string" required = true display_name = "客户编码" unique = true @@ -62,14 +62,14 @@ display_name = "客户" [[schema.entities.fields]] name = "name" - field_type = "String" + field_type = "string" required = true display_name = "客户名称" searchable = true [[schema.entities.fields]] name = "customer_type" - field_type = "String" + field_type = "string" required = true display_name = "客户类型" ui_widget = "select" @@ -81,19 +81,19 @@ display_name = "客户" [[schema.entities.fields]] name = "industry" - field_type = "String" + field_type = "string" display_name = "行业" filterable = true [[schema.entities.fields]] name = "region" - field_type = "String" + field_type = "string" display_name = "地区" filterable = true [[schema.entities.fields]] name = "source" - field_type = "String" + field_type = "string" display_name = "来源" ui_widget = "select" options = [ @@ -106,7 +106,7 @@ display_name = "客户" [[schema.entities.fields]] name = "level" - field_type = "String" + field_type = "string" display_name = "等级" ui_widget = "select" filterable = true @@ -119,7 +119,7 @@ display_name = "客户" [[schema.entities.fields]] name = "status" - field_type = "String" + field_type = "string" required = true display_name = "状态" ui_widget = "select" @@ -132,34 +132,34 @@ display_name = "客户" [[schema.entities.fields]] name = "credit_code" - field_type = "String" + field_type = "string" display_name = "统一社会信用代码" visible_when = "customer_type == 'enterprise'" [[schema.entities.fields]] name = "id_number" - field_type = "String" + field_type = "string" display_name = "身份证号" visible_when = "customer_type == 'personal'" [[schema.entities.fields]] name = "parent_id" - field_type = "Uuid" + field_type = "uuid" display_name = "上级客户" [[schema.entities.fields]] name = "website" - field_type = "String" + field_type = "string" display_name = "网站" [[schema.entities.fields]] name = "address" - field_type = "String" + field_type = "string" display_name = "地址" [[schema.entities.fields]] name = "remark" - field_type = "String" + field_type = "string" display_name = "备注" ui_widget = "textarea" @@ -169,50 +169,50 @@ display_name = "联系人" [[schema.entities.fields]] name = "customer_id" - field_type = "Uuid" + field_type = "uuid" required = true display_name = "所属客户" [[schema.entities.fields]] name = "name" - field_type = "String" + field_type = "string" required = true display_name = "姓名" searchable = true [[schema.entities.fields]] name = "position" - field_type = "String" + field_type = "string" display_name = "职务" [[schema.entities.fields]] name = "department" - field_type = "String" + field_type = "string" display_name = "部门" [[schema.entities.fields]] name = "phone" - field_type = "String" + field_type = "string" display_name = "手机号" [[schema.entities.fields]] name = "email" - field_type = "String" + field_type = "string" display_name = "邮箱" [[schema.entities.fields]] name = "wechat" - field_type = "String" + field_type = "string" display_name = "微信号" [[schema.entities.fields]] name = "is_primary" - field_type = "Boolean" + field_type = "boolean" display_name = "主联系人" [[schema.entities.fields]] name = "remark" - field_type = "String" + field_type = "string" display_name = "备注" [[schema.entities]] @@ -221,18 +221,18 @@ display_name = "沟通记录" [[schema.entities.fields]] name = "customer_id" - field_type = "Uuid" + field_type = "uuid" required = true display_name = "关联客户" [[schema.entities.fields]] name = "contact_id" - field_type = "Uuid" + field_type = "uuid" display_name = "关联联系人" [[schema.entities.fields]] name = "type" - field_type = "String" + field_type = "string" required = true display_name = "类型" ui_widget = "select" @@ -247,28 +247,28 @@ display_name = "沟通记录" [[schema.entities.fields]] name = "subject" - field_type = "String" + field_type = "string" required = true display_name = "主题" searchable = true [[schema.entities.fields]] name = "content" - field_type = "String" + field_type = "string" required = true display_name = "内容" ui_widget = "textarea" [[schema.entities.fields]] name = "occurred_at" - field_type = "DateTime" + field_type = "date_time" required = true display_name = "沟通时间" sortable = true [[schema.entities.fields]] name = "next_follow_up" - field_type = "Date" + field_type = "date" display_name = "下次跟进日期" [[schema.entities]] @@ -277,20 +277,20 @@ display_name = "客户标签" [[schema.entities.fields]] name = "customer_id" - field_type = "Uuid" + field_type = "uuid" required = true display_name = "关联客户" [[schema.entities.fields]] name = "tag_name" - field_type = "String" + field_type = "string" required = true display_name = "标签名称" searchable = true [[schema.entities.fields]] name = "tag_category" - field_type = "String" + field_type = "string" display_name = "标签分类" ui_widget = "select" options = [ @@ -306,19 +306,19 @@ display_name = "客户关系" [[schema.entities.fields]] name = "from_customer_id" - field_type = "Uuid" + field_type = "uuid" required = true display_name = "源客户" [[schema.entities.fields]] name = "to_customer_id" - field_type = "Uuid" + field_type = "uuid" required = true display_name = "目标客户" [[schema.entities.fields]] name = "relationship_type" - field_type = "String" + field_type = "string" required = true display_name = "关系类型" ui_widget = "select" @@ -333,7 +333,7 @@ display_name = "客户关系" [[schema.entities.fields]] name = "description" - field_type = "String" + field_type = "string" display_name = "关系描述" # ── 页面声明 ── diff --git a/crates/erp-plugin/src/data_service.rs b/crates/erp-plugin/src/data_service.rs index e5c1406..4c345a8 100644 --- a/crates/erp-plugin/src/data_service.rs +++ b/crates/erp-plugin/src/data_service.rs @@ -6,12 +6,28 @@ use erp_core::events::EventBus; use crate::data_dto::PluginDataResp; use crate::dynamic_table::DynamicTableManager; +use crate::entity::plugin; use crate::entity::plugin_entity; use crate::error::PluginError; use crate::manifest::PluginField; 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( @@ -23,13 +39,12 @@ impl PluginDataService { db: &sea_orm::DatabaseConnection, _event_bus: &EventBus, ) -> AppResult { - // 数据校验 - let fields = resolve_entity_fields(plugin_id, entity_name, tenant_id, db).await?; + let info = resolve_entity_info(plugin_id, entity_name, tenant_id, db).await?; + let fields = info.fields()?; validate_data(&data, &fields)?; - let table_name = resolve_table_name(plugin_id, entity_name, tenant_id, db).await?; let (sql, values) = - DynamicTableManager::build_insert_sql(&table_name, tenant_id, operator_id, &data); + DynamicTableManager::build_insert_sql(&info.table_name, tenant_id, operator_id, &data); #[derive(FromQueryResult)] struct InsertResult { @@ -71,10 +86,10 @@ impl PluginDataService { sort_by: Option, sort_order: Option, ) -> AppResult<(Vec, u64)> { - let table_name = resolve_table_name(plugin_id, entity_name, tenant_id, db).await?; + let info = resolve_entity_info(plugin_id, entity_name, tenant_id, db).await?; // 获取 searchable 字段列表 - let entity_fields = resolve_entity_fields(plugin_id, entity_name, tenant_id, db).await?; + let entity_fields = info.fields()?; let search_tuple = { let searchable: Vec<&str> = entity_fields .iter() @@ -87,9 +102,9 @@ impl PluginDataService { } }; - // Count(使用基础条件,不含 filter/search 以保持计数一致性) + // Count let (count_sql, count_values) = - DynamicTableManager::build_count_sql(&table_name, tenant_id); + DynamicTableManager::build_count_sql(&info.table_name, tenant_id); #[derive(FromQueryResult)] struct CountResult { count: i64, @@ -104,10 +119,10 @@ impl PluginDataService { .map(|r| r.count as u64) .unwrap_or(0); - // Query(带过滤/搜索/排序) + // Query let offset = page.saturating_sub(1) * page_size; let (sql, values) = DynamicTableManager::build_filtered_query_sql( - &table_name, + &info.table_name, tenant_id, page_size, offset, @@ -157,8 +172,8 @@ impl PluginDataService { tenant_id: Uuid, db: &sea_orm::DatabaseConnection, ) -> AppResult { - let table_name = resolve_table_name(plugin_id, entity_name, tenant_id, db).await?; - let (sql, values) = DynamicTableManager::build_get_by_id_sql(&table_name, id, tenant_id); + let info = resolve_entity_info(plugin_id, entity_name, tenant_id, db).await?; + let (sql, values) = DynamicTableManager::build_get_by_id_sql(&info.table_name, id, tenant_id); #[derive(FromQueryResult)] struct DataRow { @@ -199,13 +214,12 @@ impl PluginDataService { db: &sea_orm::DatabaseConnection, _event_bus: &EventBus, ) -> AppResult { - // 数据校验 - let fields = resolve_entity_fields(plugin_id, entity_name, tenant_id, db).await?; + let info = resolve_entity_info(plugin_id, entity_name, tenant_id, db).await?; + let fields = info.fields()?; validate_data(&data, &fields)?; - let table_name = resolve_table_name(plugin_id, entity_name, tenant_id, db).await?; let (sql, values) = DynamicTableManager::build_update_sql( - &table_name, + &info.table_name, id, tenant_id, operator_id, @@ -249,8 +263,8 @@ impl PluginDataService { db: &sea_orm::DatabaseConnection, _event_bus: &EventBus, ) -> AppResult<()> { - let table_name = resolve_table_name(plugin_id, entity_name, tenant_id, db).await?; - let (sql, values) = DynamicTableManager::build_delete_sql(&table_name, id, tenant_id); + let info = resolve_entity_info(plugin_id, entity_name, tenant_id, db).await?; + let (sql, values) = DynamicTableManager::build_delete_sql(&info.table_name, id, tenant_id); db.execute(Statement::from_sql_and_values( sea_orm::DatabaseBackend::Postgres, @@ -271,10 +285,9 @@ impl PluginDataService { filter: Option, search: Option, ) -> AppResult { - let table_name = resolve_table_name(plugin_id, entity_name, tenant_id, db).await?; + let info = resolve_entity_info(plugin_id, entity_name, tenant_id, db).await?; - // 获取 searchable 字段列表,构建搜索条件 - let entity_fields = resolve_entity_fields(plugin_id, entity_name, tenant_id, db).await?; + let entity_fields = info.fields()?; let search_tuple = { let searchable: Vec<&str> = entity_fields .iter() @@ -288,7 +301,7 @@ impl PluginDataService { }; let (sql, values) = DynamicTableManager::build_filtered_count_sql( - &table_name, + &info.table_name, tenant_id, filter, search_tuple, @@ -323,10 +336,10 @@ impl PluginDataService { group_by_field: &str, filter: Option, ) -> AppResult> { - let table_name = resolve_table_name(plugin_id, entity_name, tenant_id, db).await?; + let info = resolve_entity_info(plugin_id, entity_name, tenant_id, db).await?; let (sql, values) = DynamicTableManager::build_aggregate_sql( - &table_name, + &info.table_name, tenant_id, group_by_field, filter, @@ -356,13 +369,34 @@ impl PluginDataService { } } -/// 从 plugin_entities 表解析 table_name(带租户隔离) -async fn resolve_table_name( +/// 从 plugins 表解析 manifest metadata.id(如 "erp-crm") +pub async fn resolve_manifest_id( + plugin_id: Uuid, + tenant_id: Uuid, + db: &sea_orm::DatabaseConnection, +) -> AppResult { + let model = plugin::Entity::find() + .filter(plugin::Column::Id.eq(plugin_id)) + .filter(plugin::Column::TenantId.eq(tenant_id)) + .filter(plugin::Column::DeletedAt.is_null()) + .one(db) + .await? + .ok_or_else(|| AppError::NotFound(format!("插件 {} 不存在", plugin_id)))?; + + let manifest: crate::manifest::PluginManifest = + serde_json::from_value(model.manifest_json) + .map_err(|e| AppError::Internal(format!("解析插件 manifest 失败: {}", e)))?; + + Ok(manifest.metadata.id) +} + +/// 从 plugin_entities 表获取实体完整信息(带租户隔离) +async fn resolve_entity_info( plugin_id: Uuid, entity_name: &str, tenant_id: Uuid, db: &sea_orm::DatabaseConnection, -) -> AppResult { +) -> AppResult { let entity = plugin_entity::Entity::find() .filter(plugin_entity::Column::PluginId.eq(plugin_id)) .filter(plugin_entity::Column::TenantId.eq(tenant_id)) @@ -374,32 +408,10 @@ async fn resolve_table_name( AppError::NotFound(format!("插件实体 {}/{} 不存在", plugin_id, entity_name)) })?; - Ok(entity.table_name) -} - -/// 从 plugin_entities 表获取 entity 的字段定义 -async fn resolve_entity_fields( - plugin_id: Uuid, - entity_name: &str, - tenant_id: Uuid, - db: &sea_orm::DatabaseConnection, -) -> AppResult> { - 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? - .ok_or_else(|| { - AppError::NotFound(format!("插件实体 {}/{} 不存在", plugin_id, entity_name)) - })?; - - let entity_def: crate::manifest::PluginEntity = - serde_json::from_value(entity_model.schema_json) - .map_err(|e| AppError::Internal(format!("解析 entity schema 失败: {}", e)))?; - - Ok(entity_def.fields) + Ok(EntityInfo { + table_name: entity.table_name, + schema_json: entity.schema_json, + }) } /// 校验数据:检查 required 字段 diff --git a/crates/erp-plugin/src/dynamic_table.rs b/crates/erp-plugin/src/dynamic_table.rs index ee70990..9f4b49a 100644 --- a/crates/erp-plugin/src/dynamic_table.rs +++ b/crates/erp-plugin/src/dynamic_table.rs @@ -45,28 +45,25 @@ impl DynamicTableManager { \"version\" INT NOT NULL DEFAULT 1)" ); - db.execute(Statement::from_string( - sea_orm::DatabaseBackend::Postgres, - create_sql, - )) - .await - .map_err(|e| PluginError::DatabaseError(e.to_string()))?; + db.execute_unprepared(&create_sql).await + .map_err(|e| { + tracing::error!(sql = %create_sql, error = %e, "CREATE TABLE failed"); + PluginError::DatabaseError(e.to_string()) + })?; // 创建租户索引 let tenant_idx_sql = format!( "CREATE INDEX IF NOT EXISTS \"idx_{t}_tenant\" ON \"{table_name}\" (\"tenant_id\") WHERE \"deleted_at\" IS NULL", t = sanitize_identifier(&table_name) ); - db.execute(Statement::from_string( - sea_orm::DatabaseBackend::Postgres, - tenant_idx_sql, - )) - .await + db.execute_unprepared(&tenant_idx_sql).await .map_err(|e| PluginError::DatabaseError(e.to_string()))?; // 为字段创建索引(使用参数化方式避免 SQL 注入) + let mut idx_counter = 0usize; for field in &entity.fields { if field.unique || field.required { + idx_counter += 1; let sanitized_field = sanitize_identifier(&field.name); let idx_name = format!( "idx_{}_{}_{}", @@ -77,37 +74,42 @@ impl DynamicTableManager { // unique 字段使用 CREATE UNIQUE INDEX,由数据库保证数据完整性 let idx_sql = if field.unique { format!( - "CREATE UNIQUE INDEX IF NOT EXISTS \"{idx_name}\" ON \"{table_name}\" (\"data\"->>'{sanitized_field}') WHERE \"deleted_at\" IS NULL" + "CREATE UNIQUE INDEX IF NOT EXISTS \"{}\" ON \"{}\" (\"data\"->>'{}') WHERE \"deleted_at\" IS NULL", + idx_name, table_name, sanitized_field ) } else { format!( - "CREATE INDEX IF NOT EXISTS \"{idx_name}\" ON \"{table_name}\" (\"data\"->>'{sanitized_field}') WHERE \"deleted_at\" IS NULL" + "CREATE INDEX IF NOT EXISTS \"{}\" ON \"{}\" (\"data\"->>'{}') WHERE \"deleted_at\" IS NULL", + idx_name, table_name, sanitized_field ) }; - db.execute(Statement::from_string( - sea_orm::DatabaseBackend::Postgres, - idx_sql, - )) - .await - .map_err(|e| PluginError::DatabaseError(e.to_string()))?; + tracing::info!(step = idx_counter, field = %field.name, unique = field.unique, sql = %idx_sql, "Creating field index"); + match db.execute_unprepared(&idx_sql).await { + Ok(_) => {}, + Err(e) => { + tracing::warn!(step = idx_counter, field = %field.name, sql = %idx_sql, error = %e, "Index creation skipped (non-fatal)"); + } + } } } // 为 searchable 字段创建 B-tree 索引以加速 ILIKE 前缀查询 for field in &entity.fields { if field.searchable == Some(true) { + idx_counter += 1; let sanitized_field = sanitize_identifier(&field.name); let idx_name = format!("{}_{}_sidx", sanitize_identifier(&table_name), sanitized_field); let idx_sql = format!( "CREATE INDEX IF NOT EXISTS \"{}\" ON \"{}\" (\"data\"->>'{}') WHERE \"deleted_at\" IS NULL", idx_name, table_name, sanitized_field ); - db.execute(Statement::from_string( - sea_orm::DatabaseBackend::Postgres, - idx_sql, - )) - .await - .map_err(|e| PluginError::DatabaseError(e.to_string()))?; + tracing::info!(step = idx_counter, field = %field.name, sql = %idx_sql, "Creating search index"); + match db.execute_unprepared(&idx_sql).await { + Ok(_) => {}, + Err(e) => { + tracing::warn!(step = idx_counter, field = %field.name, sql = %idx_sql, error = %e, "Search index creation skipped (non-fatal)"); + } + } } } @@ -172,7 +174,7 @@ impl DynamicTableManager { ) -> (String, Vec) { let sql = format!( "INSERT INTO \"{}\" (id, tenant_id, data, created_by, updated_by, version) \ - VALUES ($1, $2, $3, $4, $5, 1) \ + VALUES ($1, $2, $3::jsonb, $4, $5, 1) \ RETURNING id, tenant_id, data, created_at, updated_at, version", table_name ); @@ -226,7 +228,7 @@ impl DynamicTableManager { ) -> (String, Vec) { let sql = format!( "UPDATE \"{}\" \ - SET data = $1, updated_at = NOW(), updated_by = $2, version = version + 1 \ + SET data = $1::jsonb, updated_at = NOW(), updated_by = $2, version = version + 1 \ WHERE id = $3 AND tenant_id = $4 AND version = $5 AND deleted_at IS NULL \ RETURNING id, data, created_at, updated_at, version", table_name diff --git a/crates/erp-plugin/src/handler/data_handler.rs b/crates/erp-plugin/src/handler/data_handler.rs index fad865f..a545733 100644 --- a/crates/erp-plugin/src/handler/data_handler.rs +++ b/crates/erp-plugin/src/handler/data_handler.rs @@ -11,17 +11,17 @@ use crate::data_dto::{ AggregateItem, AggregateQueryParams, CountQueryParams, CreatePluginDataReq, PluginDataListParams, PluginDataResp, UpdatePluginDataReq, }; -use crate::data_service::PluginDataService; +use crate::data_service::{PluginDataService, resolve_manifest_id}; use crate::state::PluginState; /// 计算插件数据操作所需的权限码 -/// 格式:{plugin_id}.{entity}.{action},如 crm.customer.list -fn compute_permission_code(plugin_id: &str, entity_name: &str, action: &str) -> String { +/// 格式:{manifest_id}.{entity}.{action},如 erp-crm.customer.list +fn compute_permission_code(manifest_id: &str, entity_name: &str, action: &str) -> String { let action_suffix = match action { "list" | "get" => "list", _ => "manage", }; - format!("{}.{}.{}", plugin_id, entity_name, action_suffix) + format!("{}.{}.{}", manifest_id, entity_name, action_suffix) } #[utoipa::path( @@ -45,8 +45,8 @@ where PluginState: FromRef, S: Clone + Send + Sync + 'static, { - // 动态权限检查:先尝试精细权限,回退到通用权限 - let fine_perm = compute_permission_code(&plugin_id.to_string(), &entity, "list"); + let manifest_id = resolve_manifest_id(plugin_id, ctx.tenant_id, &state.db).await?; + let fine_perm = compute_permission_code(&manifest_id, &entity, "list"); if require_permission(&ctx, &fine_perm).is_err() { require_permission(&ctx, "plugin.list")?; } @@ -104,7 +104,8 @@ where PluginState: FromRef, S: Clone + Send + Sync + 'static, { - let fine_perm = compute_permission_code(&plugin_id.to_string(), &entity, "create"); + let manifest_id = resolve_manifest_id(plugin_id, ctx.tenant_id, &state.db).await?; + let fine_perm = compute_permission_code(&manifest_id, &entity, "create"); if require_permission(&ctx, &fine_perm).is_err() { require_permission(&ctx, "plugin.admin")?; } @@ -142,7 +143,8 @@ where PluginState: FromRef, S: Clone + Send + Sync + 'static, { - let fine_perm = compute_permission_code(&plugin_id.to_string(), &entity, "get"); + let manifest_id = resolve_manifest_id(plugin_id, ctx.tenant_id, &state.db).await?; + let fine_perm = compute_permission_code(&manifest_id, &entity, "get"); if require_permission(&ctx, &fine_perm).is_err() { require_permission(&ctx, "plugin.list")?; } @@ -174,7 +176,8 @@ where PluginState: FromRef, S: Clone + Send + Sync + 'static, { - let fine_perm = compute_permission_code(&plugin_id.to_string(), &entity, "update"); + let manifest_id = resolve_manifest_id(plugin_id, ctx.tenant_id, &state.db).await?; + let fine_perm = compute_permission_code(&manifest_id, &entity, "update"); if require_permission(&ctx, &fine_perm).is_err() { require_permission(&ctx, "plugin.admin")?; } @@ -214,7 +217,8 @@ where PluginState: FromRef, S: Clone + Send + Sync + 'static, { - let fine_perm = compute_permission_code(&plugin_id.to_string(), &entity, "delete"); + let manifest_id = resolve_manifest_id(plugin_id, ctx.tenant_id, &state.db).await?; + let fine_perm = compute_permission_code(&manifest_id, &entity, "delete"); if require_permission(&ctx, &fine_perm).is_err() { require_permission(&ctx, "plugin.admin")?; } @@ -253,7 +257,8 @@ where PluginState: FromRef, S: Clone + Send + Sync + 'static, { - let fine_perm = compute_permission_code(&plugin_id.to_string(), &entity, "list"); + let manifest_id = resolve_manifest_id(plugin_id, ctx.tenant_id, &state.db).await?; + let fine_perm = compute_permission_code(&manifest_id, &entity, "list"); if require_permission(&ctx, &fine_perm).is_err() { require_permission(&ctx, "plugin.list")?; } @@ -298,7 +303,8 @@ where PluginState: FromRef, S: Clone + Send + Sync + 'static, { - let fine_perm = compute_permission_code(&plugin_id.to_string(), &entity, "list"); + let manifest_id = resolve_manifest_id(plugin_id, ctx.tenant_id, &state.db).await?; + let fine_perm = compute_permission_code(&manifest_id, &entity, "list"); if require_permission(&ctx, &fine_perm).is_err() { require_permission(&ctx, "plugin.list")?; } diff --git a/crates/erp-plugin/src/handler/plugin_handler.rs b/crates/erp-plugin/src/handler/plugin_handler.rs index 6e6d06d..08ec38f 100644 --- a/crates/erp-plugin/src/handler/plugin_handler.rs +++ b/crates/erp-plugin/src/handler/plugin_handler.rs @@ -199,7 +199,11 @@ where &state.db, &state.engine, ) - .await?; + .await + .map_err(|e| { + tracing::error!(error = %e, "Install failed"); + e + })?; Ok(Json(ApiResponse::ok(result))) } diff --git a/crates/erp-plugin/src/service.rs b/crates/erp-plugin/src/service.rs index ee00b34..7fb7e07 100644 --- a/crates/erp-plugin/src/service.rs +++ b/crates/erp-plugin/src/service.rs @@ -96,11 +96,16 @@ impl PluginService { // 创建动态表 + 注册 entity 记录 let mut entity_resps = Vec::new(); if let Some(schema) = &manifest.schema { - for entity_def in &schema.entities { + 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?; + 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(); @@ -143,6 +148,7 @@ impl PluginService { } // 注册插件声明的权限到 permissions 表 + tracing::info!("Registering plugin permissions"); if let Some(perms) = &manifest.permissions { register_plugin_permissions( db, @@ -152,10 +158,15 @@ impl PluginService { perms, &now, ) - .await?; + .await + .map_err(|e| { + tracing::error!(error = %e, "Failed to register permissions"); + e + })?; } // 加载到内存 + tracing::info!(manifest_id = %manifest.metadata.id, "Loading plugin into engine"); engine .load( &manifest.metadata.id, @@ -454,7 +465,22 @@ impl PluginService { let manifest: PluginManifest = serde_json::from_value(model.manifest_json.clone()) .map_err(|e| PluginError::InvalidManifest(e.to_string()))?; - Ok(serde_json::to_value(&manifest.schema).unwrap_or_default()) + + // 构建 schema 响应:entities + ui 页面配置 + 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(), + ); + } + Ok(serde_json::Value::Object(result)) } /// 清除插件记录(软删除,仅限已卸载状态) @@ -465,7 +491,7 @@ impl PluginService { db: &sea_orm::DatabaseConnection, ) -> AppResult<()> { let model = find_plugin(plugin_id, tenant_id, db).await?; - validate_status(&model.status, "uninstalled")?; + validate_status_any(&model.status, &["uninstalled", "uploaded"])?; let now = Utc::now(); let mut active: plugin::ActiveModel = model.into(); active.deleted_at = Set(Some(now)); @@ -585,34 +611,33 @@ async fn register_plugin_permissions( ) -> AppResult<()> { for perm in perms { let full_code = format!("{}.{}", plugin_manifest_id, perm.code); - // resource 使用插件 manifest id,action 使用权限的 code 字段 let resource = plugin_manifest_id.to_string(); let action = perm.code.clone(); - let description_sql = if perm.description.is_empty() { - "NULL".to_string() + let description: Option = if perm.description.is_empty() { + None } else { - format!("'{}'", perm.description.replace('\'', "''")) + Some(perm.description.clone()) }; - let sql = format!( - r#" + 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(), '{tenant_id}', '{full_code}', '{name}', '{resource}', '{action}', {desc}, '{now}', '{now}', '{operator_id}', '{operator_id}', NULL, 1) + 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 - "#, - tenant_id = tenant_id, - full_code = full_code.replace('\'', "''"), - name = perm.name.replace('\'', "''"), - resource = resource.replace('\'', "''"), - action = action.replace('\'', "''"), - desc = description_sql, - now = now.to_rfc3339(), - operator_id = operator_id, - ); + "#; - db.execute(sea_orm::Statement::from_string( + 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| { @@ -645,28 +670,28 @@ async fn unregister_plugin_permissions( plugin_manifest_id: &str, ) -> AppResult<()> { let prefix = format!("{}.%", plugin_manifest_id); - let now = chrono::Utc::now().to_rfc3339(); + let now = chrono::Utc::now(); // 先软删除 role_permissions 中的关联 - let rp_sql = format!( - r#" + let rp_sql = r#" UPDATE role_permissions - SET deleted_at = '{now}', updated_at = '{now}' + SET deleted_at = $1, updated_at = $1 WHERE permission_id IN ( SELECT id FROM permissions - WHERE tenant_id = '{tenant_id}' - AND code LIKE '{prefix}' + WHERE tenant_id = $2 + AND code LIKE $3 AND deleted_at IS NULL ) AND deleted_at IS NULL - "#, - now = now, - tenant_id = tenant_id, - prefix = prefix.replace('\'', "''"), - ); - db.execute(sea_orm::Statement::from_string( + "#; + 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| { @@ -679,21 +704,21 @@ async fn unregister_plugin_permissions( })?; // 再软删除 permissions - let perm_sql = format!( - r#" + let perm_sql = r#" UPDATE permissions - SET deleted_at = '{now}', updated_at = '{now}' - WHERE tenant_id = '{tenant_id}' - AND code LIKE '{prefix}' + SET deleted_at = $1, updated_at = $1 + WHERE tenant_id = $2 + AND code LIKE $3 AND deleted_at IS NULL - "#, - now = now, - tenant_id = tenant_id, - prefix = prefix.replace('\'', "''"), - ); - db.execute(sea_orm::Statement::from_string( + "#; + 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| {