feat(plugin): P1 跨插件数据引用系统 — 后端 Phase 1-3
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled

实现跨插件实体引用的基础后端能力:

Phase 1 — Manifest 扩展 + Entity Registry 数据层:
- PluginField 新增 ref_plugin/ref_fallback_label 支持跨插件引用声明
- PluginRelation 新增 name/relation_type/display_field(CRM 已在用的字段)
- PluginEntity 新增 is_public 标记可被其他插件引用的实体
- 数据库迁移:plugin_entities 新增 manifest_id + is_public 列 + 索引
- SeaORM Entity 和 install 流程同步更新

Phase 2 — 后端跨插件引用解析 + 校验:
- data_service: 新增 resolve_cross_plugin_entity/is_plugin_active 函数
- validate_ref_entities: 支持 ref_plugin 字段,目标插件未安装时跳过校验(软警告)
- host.rs: HostState 新增 cross_plugin_entities 映射,db_query 支持点分记号
- engine.rs: execute_wasm 自动构建跨插件实体映射

Phase 3 — API 端点:
- POST /plugins/{id}/{entity}/resolve-labels 批量标签解析
- GET /plugin-registry/entities 公开实体注册表查询
This commit is contained in:
iven
2026-04-19 00:49:00 +08:00
parent 1dbda4c1e8
commit ef89ed38a1
12 changed files with 1425 additions and 24 deletions

View File

@@ -4,7 +4,7 @@ use uuid::Uuid;
use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
use crate::error::{PluginError, PluginResult};
use crate::manifest::{PluginEntity, PluginFieldType};
use crate::manifest::{PluginEntity, PluginField, PluginFieldType};
/// 消毒标识符:只保留 ASCII 字母、数字、下划线,防止 SQL 注入
pub(crate) fn sanitize_identifier(input: &str) -> String {
@@ -14,6 +14,13 @@ pub(crate) fn sanitize_identifier(input: &str) -> String {
.collect()
}
/// Schema 演进字段差异
pub struct FieldDiff {
pub new_filterable: Vec<PluginField>,
pub new_sortable: Vec<PluginField>,
pub new_searchable: Vec<PluginField>,
}
/// 动态表管理器 — 处理插件动态创建/删除的数据库表
pub struct DynamicTableManager;
@@ -158,6 +165,102 @@ impl DynamicTableManager {
Ok(())
}
/// Schema 演进:对比新旧实体字段,返回需要新增 Generated Column 的差异
pub fn diff_entity_fields(old: &PluginEntity, new: &PluginEntity) -> FieldDiff {
let old_names: std::collections::HashSet<String> =
old.fields.iter().map(|f| f.name.clone()).collect();
let mut new_filterable = Vec::new();
let mut new_sortable = Vec::new();
let mut new_searchable = Vec::new();
for field in &new.fields {
if old_names.contains(&field.name) {
continue;
}
// 新增字段 + 需要 Generated Column 的条件
let needs_gen = field.unique
|| field.sortable == Some(true)
|| field.filterable == Some(true);
if needs_gen {
new_filterable.push(field.clone());
if field.sortable == Some(true) {
new_sortable.push(field.clone());
}
}
if field.searchable == Some(true) && matches!(field.field_type, PluginFieldType::String) {
new_searchable.push(field.clone());
}
}
FieldDiff { new_filterable, new_sortable, new_searchable }
}
/// Schema 演进:为已有实体新增 Generated Column 和索引
pub async fn alter_add_generated_columns(
db: &DatabaseConnection,
plugin_id: &str,
entity: &PluginEntity,
diff: &FieldDiff,
) -> PluginResult<()> {
let table_name = Self::table_name(plugin_id, &entity.name);
let mut statements = Vec::new();
for field in &diff.new_filterable {
if !field.field_type.supports_generated_column() {
continue;
}
let col_name = format!("_f_{}", sanitize_identifier(&field.name));
let sql_type = field.field_type.generated_sql_type();
let expr = field.field_type.generated_expr(&sanitize_identifier(&field.name));
let _safe_field = sanitize_identifier(&field.name);
statements.push(format!(
"ALTER TABLE \"{}\" ADD COLUMN IF NOT EXISTS \"{}\" {} GENERATED ALWAYS AS ({}) STORED",
table_name, col_name, sql_type, expr
));
let col_idx = format!("{}_{}", sanitize_identifier(&table_name), col_name);
if field.unique {
statements.push(format!(
"CREATE UNIQUE INDEX IF NOT EXISTS \"idx_{}_uniq\" ON \"{}\" (tenant_id, \"{}\") WHERE deleted_at IS NULL",
col_idx, table_name, col_name
));
} else {
statements.push(format!(
"CREATE INDEX IF NOT EXISTS \"idx_{}\" ON \"{}\" (tenant_id, \"{}\") WHERE deleted_at IS NULL",
col_idx, table_name, col_name
));
}
}
for field in &diff.new_searchable {
let sf = sanitize_identifier(&field.name);
let col_name = format!("_f_{}", sf);
let col_idx = format!("{}_{}trgm", sanitize_identifier(&table_name), col_name);
statements.push(format!(
"CREATE INDEX IF NOT EXISTS \"idx_{}\" ON \"{}\" USING gin (\"{}\" gin_trgm_ops) WHERE deleted_at IS NULL AND \"{}\" IS NOT NULL",
col_idx, table_name, col_name, col_name
));
}
for sql in &statements {
tracing::info!(sql = %sql, "Executing ALTER TABLE");
db.execute_unprepared(sql).await.map_err(|e| {
tracing::error!(sql = %sql, error = %e, "ALTER TABLE failed");
PluginError::DatabaseError(e.to_string())
})?;
}
tracing::info!(
table = %table_name,
added_columns = diff.new_filterable.len(),
added_search_indexes = diff.new_searchable.len(),
"Schema evolution: Generated Columns added"
);
Ok(())
}
/// 检查表是否存在
pub async fn table_exists(db: &DatabaseConnection, table_name: &str) -> PluginResult<bool> {
#[derive(FromQueryResult)]
@@ -461,6 +564,82 @@ impl DynamicTableManager {
Ok((sql, values))
}
/// 构建多聚合函数 SQL支持 COUNT/SUM/AVG/MIN/MAX
pub fn build_aggregate_multi_sql(
table_name: &str,
tenant_id: Uuid,
group_by_field: &str,
aggregations: &[(String, String)], // (func, field) e.g. ("sum", "amount")
filter: Option<serde_json::Value>,
) -> Result<(String, Vec<Value>), String> {
let clean_group = sanitize_identifier(group_by_field);
if clean_group.is_empty() {
return Err(format!("无效的分组字段名: {}", group_by_field));
}
let mut conditions = vec![
format!("\"tenant_id\" = ${}", 1),
"\"deleted_at\" IS NULL".to_string(),
];
let mut param_idx = 2;
let mut values: Vec<Value> = vec![tenant_id.into()];
if let Some(f) = filter {
if let Some(obj) = f.as_object() {
for (key, val) in obj {
let clean_key = sanitize_identifier(key);
if clean_key.is_empty() {
return Err(format!("无效的过滤字段名: {}", key));
}
conditions.push(format!("\"data\"->>'{}' = ${}", clean_key, param_idx));
values.push(Value::String(Some(Box::new(
val.as_str().unwrap_or("").to_string(),
))));
param_idx += 1;
}
}
}
let mut select_parts = vec![
format!("\"_f_{}\" as key", clean_group),
"COUNT(*) as count".to_string(),
];
for (func, field) in aggregations {
let clean_field = sanitize_identifier(field);
let func_lower = func.to_lowercase();
match func_lower.as_str() {
"sum" => select_parts.push(format!(
"COALESCE(SUM(\"_f_{}\"), 0) as sum_{}", clean_field, clean_field
)),
"avg" => select_parts.push(format!(
"COALESCE(AVG(\"_f_{}\"), 0) as avg_{}", clean_field, clean_field
)),
"min" => select_parts.push(format!(
"MIN(\"_f_{}\") as min_{}", clean_field, clean_field
)),
"max" => select_parts.push(format!(
"MAX(\"_f_{}\") as max_{}", clean_field, clean_field
)),
_ => {}
}
}
let sql = format!(
"SELECT {} \
FROM \"{}\" \
WHERE {} \
GROUP BY \"_f_{}\" \
ORDER BY count DESC",
select_parts.join(", "),
table_name,
conditions.join(" AND "),
clean_group,
);
Ok((sql, values))
}
/// 构建带过滤条件的查询 SQL
pub fn build_filtered_query_sql(
table_name: &str,
@@ -1132,6 +1311,7 @@ mod tests {
indexes: vec![],
relations: vec![],
data_scope: None,
is_public: None,
};
let sql = DynamicTableManager::build_create_table_sql("erp_crm", &entity);
@@ -1174,6 +1354,7 @@ mod tests {
indexes: vec![],
relations: vec![],
data_scope: None,
is_public: None,
};
let sql = DynamicTableManager::build_create_table_sql("erp_crm", &entity);