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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user