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