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

@@ -9,7 +9,8 @@ use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
use crate::data_dto::{
AggregateItem, AggregateMultiReq, AggregateMultiRow, AggregateQueryParams, BatchActionReq,
CountQueryParams, CreatePluginDataReq, PatchPluginDataReq, PluginDataListParams,
CountQueryParams, CreatePluginDataReq, ExportParams, ImportReq, ImportResult,
PatchPluginDataReq, PluginDataListParams,
PluginDataResp, PublicEntityResp, ResolveLabelsReq, ResolveLabelsResp,
TimeseriesItem, TimeseriesParams, UpdatePluginDataReq,
};
@@ -780,3 +781,95 @@ where
Ok(Json(ApiResponse::ok(result)))
}
// ─── 数据导入导出 ──────────────────────────────────────────────────────
#[utoipa::path(
get,
path = "/api/v1/plugins/{plugin_id}/{entity}/export",
params(ExportParams),
responses(
(status = 200, description = "导出成功"),
),
security(("bearer_auth" = [])),
tag = "插件数据"
)]
/// GET /api/v1/plugins/{plugin_id}/{entity}/export — 导出数据
pub async fn export_plugin_data<S>(
State(state): State<PluginState>,
Extension(ctx): Extension<TenantContext>,
Path((plugin_id, entity)): Path<(Uuid, String)>,
Query(params): Query<ExportParams>,
) -> Result<Json<ApiResponse<Vec<serde_json::Value>>>, AppError>
where
PluginState: FromRef<S>,
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(&manifest_id, &entity, "list");
require_permission(&ctx, &fine_perm)?;
let scope = resolve_data_scope(
&ctx, &manifest_id, &entity, &fine_perm, &state.db,
).await?;
let filter: Option<serde_json::Value> = params
.filter
.as_ref()
.and_then(|f| serde_json::from_str(f).ok());
let rows = PluginDataService::export(
plugin_id,
&entity,
ctx.tenant_id,
&state.db,
filter,
params.search,
params.sort_by,
params.sort_order,
&state.entity_cache,
scope,
)
.await?;
Ok(Json(ApiResponse::ok(rows)))
}
#[utoipa::path(
post,
path = "/api/v1/plugins/{plugin_id}/{entity}/import",
request_body = ImportReq,
responses(
(status = 200, description = "导入完成", body = ApiResponse<ImportResult>),
),
security(("bearer_auth" = [])),
tag = "插件数据"
)]
/// POST /api/v1/plugins/{plugin_id}/{entity}/import — 导入数据
pub async fn import_plugin_data<S>(
State(state): State<PluginState>,
Extension(ctx): Extension<TenantContext>,
Path((plugin_id, entity)): Path<(Uuid, String)>,
Json(req): Json<ImportReq>,
) -> Result<Json<ApiResponse<ImportResult>>, AppError>
where
PluginState: FromRef<S>,
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(&manifest_id, &entity, "create");
require_permission(&ctx, &fine_perm)?;
let result = PluginDataService::import(
plugin_id,
&entity,
ctx.tenant_id,
ctx.user_id,
req.rows,
&state.db,
&state.event_bus,
)
.await?;
Ok(Json(ApiResponse::ok(result)))
}