feat(plugin): P2-4 数据导入导出 — 后端 export/import API + 前端 UI + TS 修复

- data_service: export 方法查询匹配行(上限10000),import 方法逐行校验+插入
- data_handler: export_plugin_data / import_plugin_data 处理函数
- module: 注册 GET /export + POST /import 路由
- pluginData.ts: exportPluginData / importPluginData API 函数
- PluginCRUDPage: 根据 entity importable/exportable 标志显示导出/导入按钮
- PluginMarket: 修复 TS 错误 (unused imports, type narrowing)
- PluginSettingsForm: 修复 TS 错误 (Rule type, Divider orientation)
This commit is contained in:
iven
2026-04-19 13:28:12 +08:00
parent e429448c42
commit 120f3fe867
8 changed files with 464 additions and 6 deletions

View File

@@ -505,6 +505,139 @@ impl PluginDataService {
Ok(())
}
/// 导出数据(不分页,复用 list 的过滤逻辑)
pub async fn export(
plugin_id: Uuid,
entity_name: &str,
tenant_id: Uuid,
db: &sea_orm::DatabaseConnection,
filter: Option<serde_json::Value>,
search: Option<String>,
sort_by: Option<String>,
sort_order: Option<String>,
cache: &moka::sync::Cache<String, EntityInfo>,
scope: Option<DataScopeParams>,
) -> AppResult<Vec<serde_json::Value>> {
let info =
resolve_entity_info_cached(plugin_id, entity_name, tenant_id, db, cache).await?;
// 搜索字段
let entity_fields = info.fields()?;
let search_tuple = {
let searchable: Vec<&str> = entity_fields
.iter()
.filter(|f| f.searchable == Some(true))
.map(|f| f.name.as_str())
.collect();
match (searchable.is_empty(), &search) {
(false, Some(kw)) => Some((searchable.join(","), kw.clone())),
_ => None,
}
};
// 查询所有匹配行(上限 10000
let (sql, mut values) = DynamicTableManager::build_filtered_query_sql_ex(
&info.table_name,
tenant_id,
10000,
0,
filter,
search_tuple,
sort_by,
sort_order,
&info.generated_fields,
)
.map_err(|e| AppError::Validation(e))?;
// 注入数据权限
let scope_condition = build_scope_sql(&scope, &info.generated_fields, values.len() + 1);
let sql = merge_scope_condition(sql, &scope_condition);
values.extend(scope_condition.1);
#[derive(FromQueryResult)]
struct DataRow { data: serde_json::Value }
let rows = DataRow::find_by_statement(Statement::from_sql_and_values(
sea_orm::DatabaseBackend::Postgres,
sql,
values,
))
.all(db)
.await?;
Ok(rows.into_iter().map(|r| r.data).collect())
}
/// 批量导入数据(逐行校验 + 逐行插入)
pub async fn import(
plugin_id: Uuid,
entity_name: &str,
tenant_id: Uuid,
operator_id: Uuid,
rows: Vec<serde_json::Value>,
db: &sea_orm::DatabaseConnection,
event_bus: &EventBus,
) -> AppResult<crate::data_dto::ImportResult> {
use crate::data_dto::{ImportResult, ImportRowError};
if rows.len() > 1000 {
return Err(AppError::Validation("单次导入上限 1000 行".to_string()));
}
let info = resolve_entity_info(plugin_id, entity_name, tenant_id, db).await?;
let fields = info.fields()?;
let mut success_count = 0usize;
let mut row_errors: Vec<ImportRowError> = Vec::new();
for (i, row_data) in rows.iter().enumerate() {
if let Err(e) = validate_data(row_data, &fields) {
row_errors.push(ImportRowError { row: i, errors: vec![e.to_string()] });
continue;
}
if let Err(e) = validate_ref_entities(row_data, &fields, entity_name, plugin_id, tenant_id, db, true, None).await {
row_errors.push(ImportRowError { row: i, errors: vec![e.to_string()] });
continue;
}
let (sql, values) =
DynamicTableManager::build_insert_sql(&info.table_name, tenant_id, operator_id, row_data);
let result = db.execute(Statement::from_sql_and_values(
sea_orm::DatabaseBackend::Postgres,
sql,
values,
)).await;
match result {
Ok(_) => success_count += 1,
Err(e) => {
row_errors.push(ImportRowError { row: i, errors: vec![format!("写入失败: {}", e)] });
}
}
}
audit_service::record(
AuditLog::new(tenant_id, Some(operator_id), "plugin.data.import", entity_name),
db,
)
.await;
if let Ok(triggers) = find_trigger_events(plugin_id, entity_name, db).await {
emit_trigger_events(
&triggers, "create", entity_name,
&format!("batch_import:{}", success_count),
tenant_id, None, event_bus, db,
).await;
}
Ok(ImportResult {
success_count,
error_count: row_errors.len(),
errors: row_errors,
})
}
/// 批量操作 — batch_delete / batch_update
pub async fn batch(
plugin_id: Uuid,