use axum::Extension; use axum::extract::{FromRef, Multipart, Path, Query, State}; use axum::response::Json; use uuid::Uuid; use erp_core::error::AppError; use erp_core::rbac::require_permission; use erp_core::types::{ApiResponse, PaginatedResponse, Pagination, TenantContext}; use crate::dto::{ PluginHealthResp, PluginListParams, PluginResp, UpdatePluginConfigReq, }; use crate::service::PluginService; use crate::state::PluginState; #[utoipa::path( post, path = "/api/v1/admin/plugins/upload", request_body(content_type = "multipart/form-data"), responses( (status = 200, description = "上传成功", body = ApiResponse), (status = 401, description = "未授权"), ), security(("bearer_auth" = [])), tag = "插件管理" )] /// POST /api/v1/admin/plugins/upload — 上传插件 (multipart: wasm + manifest) pub async fn upload_plugin( State(state): State, Extension(ctx): Extension, mut multipart: Multipart, ) -> Result>, AppError> where PluginState: FromRef, S: Clone + Send + Sync + 'static, { require_permission(&ctx, "plugin.admin")?; let mut wasm_binary: Option> = None; let mut manifest_toml: Option = None; while let Some(field) = multipart.next_field().await.map_err(|e| { AppError::Validation(format!("Multipart 解析失败: {}", e)) })? { let name = field.name().unwrap_or(""); match name { "wasm" => { wasm_binary = Some(field.bytes().await.map_err(|e| { AppError::Validation(format!("读取 WASM 文件失败: {}", e)) })?.to_vec()); } "manifest" => { let bytes = field.bytes().await.map_err(|e| { AppError::Validation(format!("读取 Manifest 失败: {}", e)) })?; let text = String::from_utf8(bytes.to_vec()).map_err(|e| { AppError::Validation(format!("Manifest 不是有效的 UTF-8: {}", e)) })?; manifest_toml = Some(text); } _ => {} } } let wasm = wasm_binary.ok_or_else(|| { AppError::Validation("缺少 wasm 文件".to_string()) })?; let manifest = manifest_toml.ok_or_else(|| { AppError::Validation("缺少 manifest 文件".to_string()) })?; let result = PluginService::upload( ctx.tenant_id, ctx.user_id, wasm, &manifest, &state.db, ) .await?; Ok(Json(ApiResponse::ok(result))) } #[utoipa::path( get, path = "/api/v1/admin/plugins", params(PluginListParams), responses( (status = 200, description = "成功", body = ApiResponse>), ), security(("bearer_auth" = [])), tag = "插件管理" )] /// GET /api/v1/admin/plugins — 列表 pub async fn list_plugins( State(state): State, Extension(ctx): Extension, Query(params): Query, ) -> Result>>, AppError> where PluginState: FromRef, S: Clone + Send + Sync + 'static, { require_permission(&ctx, "plugin.list")?; let pagination = Pagination { page: params.page, page_size: params.page_size, }; let (plugins, total) = PluginService::list( ctx.tenant_id, pagination.page.unwrap_or(1), pagination.page_size.unwrap_or(20), params.status.as_deref(), params.search.as_deref(), &state.db, ) .await?; Ok(Json(ApiResponse::ok(PaginatedResponse { data: plugins, total, page: pagination.page.unwrap_or(1), page_size: pagination.page_size.unwrap_or(20), total_pages: (total as f64 / pagination.page_size.unwrap_or(20) as f64).ceil() as u64, }))) } #[utoipa::path( get, path = "/api/v1/admin/plugins/{id}", responses( (status = 200, description = "成功", body = ApiResponse), ), security(("bearer_auth" = [])), tag = "插件管理" )] /// GET /api/v1/admin/plugins/{id} — 详情 pub async fn get_plugin( State(state): State, Extension(ctx): Extension, Path(id): Path, ) -> Result>, AppError> where PluginState: FromRef, S: Clone + Send + Sync + 'static, { require_permission(&ctx, "plugin.list")?; let result = PluginService::get_by_id(id, ctx.tenant_id, &state.db).await?; Ok(Json(ApiResponse::ok(result))) } #[utoipa::path( get, path = "/api/v1/admin/plugins/{id}/schema", responses( (status = 200, description = "成功"), ), security(("bearer_auth" = [])), tag = "插件管理" )] /// GET /api/v1/admin/plugins/{id}/schema — 实体 schema pub async fn get_plugin_schema( State(state): State, Extension(ctx): Extension, Path(id): Path, ) -> Result>, AppError> where PluginState: FromRef, S: Clone + Send + Sync + 'static, { require_permission(&ctx, "plugin.list")?; let schema = PluginService::get_schema(id, ctx.tenant_id, &state.db).await?; Ok(Json(ApiResponse::ok(schema))) } #[utoipa::path( post, path = "/api/v1/admin/plugins/{id}/install", responses( (status = 200, description = "安装成功", body = ApiResponse), ), security(("bearer_auth" = [])), tag = "插件管理" )] /// POST /api/v1/admin/plugins/{id}/install — 安装 pub async fn install_plugin( State(state): State, Extension(ctx): Extension, Path(id): Path, ) -> Result>, AppError> where PluginState: FromRef, S: Clone + Send + Sync + 'static, { require_permission(&ctx, "plugin.admin")?; let result = PluginService::install( id, ctx.tenant_id, ctx.user_id, &state.db, &state.engine, ) .await .map_err(|e| { tracing::error!(error = %e, "Install failed"); e })?; Ok(Json(ApiResponse::ok(result))) } #[utoipa::path( post, path = "/api/v1/admin/plugins/{id}/enable", responses( (status = 200, description = "启用成功", body = ApiResponse), ), security(("bearer_auth" = [])), tag = "插件管理" )] /// POST /api/v1/admin/plugins/{id}/enable — 启用 pub async fn enable_plugin( State(state): State, Extension(ctx): Extension, Path(id): Path, ) -> Result>, AppError> where PluginState: FromRef, S: Clone + Send + Sync + 'static, { require_permission(&ctx, "plugin.admin")?; let result = PluginService::enable( id, ctx.tenant_id, ctx.user_id, &state.db, &state.engine, ) .await?; Ok(Json(ApiResponse::ok(result))) } #[utoipa::path( post, path = "/api/v1/admin/plugins/{id}/disable", responses( (status = 200, description = "停用成功", body = ApiResponse), ), security(("bearer_auth" = [])), tag = "插件管理" )] /// POST /api/v1/admin/plugins/{id}/disable — 停用 pub async fn disable_plugin( State(state): State, Extension(ctx): Extension, Path(id): Path, ) -> Result>, AppError> where PluginState: FromRef, S: Clone + Send + Sync + 'static, { require_permission(&ctx, "plugin.admin")?; let result = PluginService::disable( id, ctx.tenant_id, ctx.user_id, &state.db, &state.engine, ) .await?; Ok(Json(ApiResponse::ok(result))) } #[utoipa::path( post, path = "/api/v1/admin/plugins/{id}/uninstall", responses( (status = 200, description = "卸载成功", body = ApiResponse), ), security(("bearer_auth" = [])), tag = "插件管理" )] /// POST /api/v1/admin/plugins/{id}/uninstall — 卸载 pub async fn uninstall_plugin( State(state): State, Extension(ctx): Extension, Path(id): Path, ) -> Result>, AppError> where PluginState: FromRef, S: Clone + Send + Sync + 'static, { require_permission(&ctx, "plugin.admin")?; let result = PluginService::uninstall( id, ctx.tenant_id, ctx.user_id, &state.db, &state.engine, ) .await?; Ok(Json(ApiResponse::ok(result))) } #[utoipa::path( delete, path = "/api/v1/admin/plugins/{id}", responses( (status = 200, description = "清除成功"), ), security(("bearer_auth" = [])), tag = "插件管理" )] /// DELETE /api/v1/admin/plugins/{id} — 清除(软删除) pub async fn purge_plugin( State(state): State, Extension(ctx): Extension, Path(id): Path, ) -> Result>, AppError> where PluginState: FromRef, S: Clone + Send + Sync + 'static, { require_permission(&ctx, "plugin.admin")?; PluginService::purge(id, ctx.tenant_id, ctx.user_id, &state.db).await?; Ok(Json(ApiResponse::ok(()))) } #[utoipa::path( get, path = "/api/v1/admin/plugins/{id}/health", responses( (status = 200, description = "健康检查", body = ApiResponse), ), security(("bearer_auth" = [])), tag = "插件管理" )] /// GET /api/v1/admin/plugins/{id}/health — 健康检查 pub async fn health_check_plugin( State(state): State, Extension(ctx): Extension, Path(id): Path, ) -> Result>, AppError> where PluginState: FromRef, S: Clone + Send + Sync + 'static, { require_permission(&ctx, "plugin.list")?; let result = PluginService::health_check(id, ctx.tenant_id, &state.db, &state.engine).await?; Ok(Json(ApiResponse::ok(result))) } #[utoipa::path( get, path = "/api/v1/admin/plugins/{id}/metrics", responses( (status = 200, description = "运行时指标", body = ApiResponse), ), security(("bearer_auth" = [])), tag = "插件管理" )] /// GET /api/v1/admin/plugins/{id}/metrics — 运行时指标 pub async fn get_plugin_metrics( State(state): State, Extension(ctx): Extension, Path(id): Path, ) -> Result>, AppError> where PluginState: FromRef, S: Clone + Send + Sync + 'static, { require_permission(&ctx, "plugin.list")?; // 通过 plugin_id 找到 manifest_id,再查询 metrics let manifest_id = crate::data_service::resolve_manifest_id(id, ctx.tenant_id, &state.db).await?; let metrics = state.engine.get_metrics(&manifest_id).await .map_err(|e| AppError::Internal(e.to_string()))?; let avg_ms = if metrics.total_invocations > 0 { metrics.total_response_ms / metrics.total_invocations as f64 } else { 0.0 }; Ok(Json(ApiResponse::ok(serde_json::json!({ "plugin_id": manifest_id, "total_invocations": metrics.total_invocations, "error_count": metrics.error_count, "avg_response_ms": avg_ms, "last_error": metrics.last_error, "last_invocation_at": metrics.last_invocation_at, })))) } #[utoipa::path( put, path = "/api/v1/admin/plugins/{id}/config", request_body = UpdatePluginConfigReq, responses( (status = 200, description = "更新成功", body = ApiResponse), ), security(("bearer_auth" = [])), tag = "插件管理" )] /// PUT /api/v1/admin/plugins/{id}/config — 更新配置 pub async fn update_plugin_config( State(state): State, Extension(ctx): Extension, Path(id): Path, Json(req): Json, ) -> Result>, AppError> where PluginState: FromRef, S: Clone + Send + Sync + 'static, { require_permission(&ctx, "plugin.admin")?; let result = PluginService::update_config( id, ctx.tenant_id, ctx.user_id, req.config, req.version, &state.db, Some(&state.event_bus), ) .await?; Ok(Json(ApiResponse::ok(result))) } #[utoipa::path( post, path = "/api/v1/admin/plugins/{id}/upgrade", request_body(content_type = "multipart/form-data"), responses( (status = 200, description = "升级成功", body = ApiResponse), ), security(("bearer_auth" = [])), tag = "插件管理" )] /// POST /api/v1/admin/plugins/{id}/upgrade — 热更新插件 /// /// 上传新版本 WASM + manifest,对比 schema 变更,执行增量 DDL, /// 更新插件记录。失败时保持旧版本继续运行。 pub async fn upgrade_plugin( State(state): State, Extension(ctx): Extension, Path(id): Path, mut multipart: Multipart, ) -> Result>, AppError> where PluginState: FromRef, S: Clone + Send + Sync + 'static, { require_permission(&ctx, "plugin.admin")?; let mut wasm_binary: Option> = None; let mut manifest_toml: Option = None; while let Some(field) = multipart.next_field().await.map_err(|e| { AppError::Validation(format!("Multipart 解析失败: {}", e)) })? { let name = field.name().unwrap_or(""); match name { "wasm" => { wasm_binary = Some(field.bytes().await.map_err(|e| { AppError::Validation(format!("读取 WASM 文件失败: {}", e)) })?.to_vec()); } "manifest" => { let bytes = field.bytes().await.map_err(|e| { AppError::Validation(format!("读取 Manifest 失败: {}", e)) })?; manifest_toml = Some(String::from_utf8(bytes.to_vec()).map_err(|e| { AppError::Validation(format!("Manifest 不是有效的 UTF-8: {}", e)) })?); } _ => {} } } let wasm = wasm_binary.ok_or_else(|| { AppError::Validation("缺少 wasm 文件".to_string()) })?; let manifest = manifest_toml.ok_or_else(|| { AppError::Validation("缺少 manifest 文件".to_string()) })?; let result = PluginService::upgrade( id, ctx.tenant_id, ctx.user_id, wasm, &manifest, &state.db, &state.engine, ) .await?; Ok(Json(ApiResponse::ok(result))) } #[utoipa::path( get, path = "/api/v1/admin/plugins/{id}/validate", params(("id" = Uuid, Path, description = "插件 ID")), responses((status = 200, description = "安全验证报告")), security(("bearer_auth" = [])), tag = "插件管理" )] /// GET /api/v1/admin/plugins/{id}/validate — 获取插件安全验证报告 pub async fn validate_plugin( State(state): State, Extension(ctx): Extension, Path(id): Path, ) -> Result>, AppError> where PluginState: FromRef, S: Clone + Send + Sync + 'static, { require_permission(&ctx, "plugin.admin")?; let model = crate::service::find_plugin_model(id, ctx.tenant_id, &state.db).await?; let manifest: crate::manifest::PluginManifest = serde_json::from_value(model.manifest_json.clone()) .map_err(|e| AppError::Validation(format!("manifest 解析失败: {}", e)))?; let report = crate::plugin_validator::validate_plugin_security(&manifest, model.wasm_binary.len())?; Ok(Json(ApiResponse::ok(report))) }