feat(plugin): P1-P4 审计修复 — 第一批 (Excel/CSV导出 + 市场API + 对账扫描)
1.1 Excel/CSV 导出:
- 后端 export 支持 format 参数 (json/csv/xlsx)
- rust_xlsxwriter 生成带样式 Excel
- 前端导出按钮改为 Dropdown 格式选择 (JSON/CSV/Excel)
- blob 下载支持 CSV/XLSX 二进制格式
1.2 市场后端 API + 前端对接:
- SeaORM Entity: market_entry, market_review
- API: 浏览/详情/一键安装/评论列表/提交评分
- 一键安装: upload → install → enable 一条龙 + 依赖检查
- 前端 PluginMarket 对接真实 API (搜索/分类/安装/评分)
1.3 对账扫描:
- reconcile_references() 扫描跨插件引用悬空 UUID
- POST /plugins/{plugin_id}/reconcile 端点
This commit is contained in:
@@ -11,7 +11,7 @@ use crate::data_dto::{
|
||||
AggregateItem, AggregateMultiReq, AggregateMultiRow, AggregateQueryParams, BatchActionReq,
|
||||
CountQueryParams, CreatePluginDataReq, ExportParams, ImportReq, ImportResult,
|
||||
PatchPluginDataReq, PluginDataListParams,
|
||||
PluginDataResp, PublicEntityResp, ResolveLabelsReq, ResolveLabelsResp,
|
||||
PluginDataResp, PublicEntityResp, ReconciliationReport, ResolveLabelsReq, ResolveLabelsResp,
|
||||
TimeseriesItem, TimeseriesParams, UpdatePluginDataReq,
|
||||
};
|
||||
use crate::data_service::{DataScopeParams, PluginDataService, resolve_manifest_id};
|
||||
@@ -794,17 +794,21 @@ where
|
||||
security(("bearer_auth" = [])),
|
||||
tag = "插件数据"
|
||||
)]
|
||||
/// GET /api/v1/plugins/{plugin_id}/{entity}/export — 导出数据
|
||||
/// GET /api/v1/plugins/{plugin_id}/{entity}/export — 导出数据 (JSON/CSV/XLSX)
|
||||
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>
|
||||
) -> Result<axum::response::Response, AppError>
|
||||
where
|
||||
PluginState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
use crate::data_dto::ExportPayload;
|
||||
use axum::http::{header, StatusCode};
|
||||
use axum::body::Body;
|
||||
|
||||
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)?;
|
||||
@@ -818,7 +822,7 @@ where
|
||||
.as_ref()
|
||||
.and_then(|f| serde_json::from_str(f).ok());
|
||||
|
||||
let rows = PluginDataService::export(
|
||||
let payload = PluginDataService::export(
|
||||
plugin_id,
|
||||
&entity,
|
||||
ctx.tenant_id,
|
||||
@@ -827,12 +831,40 @@ where
|
||||
params.search,
|
||||
params.sort_by,
|
||||
params.sort_order,
|
||||
params.format,
|
||||
&state.entity_cache,
|
||||
scope,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(Json(ApiResponse::ok(rows)))
|
||||
let filename = format!("{}_export_{}", entity, chrono::Utc::now().format("%Y%m%d%H%M%S"));
|
||||
match payload {
|
||||
ExportPayload::Json(data) => {
|
||||
let body = serde_json::to_string(&ApiResponse::ok(data))
|
||||
.map_err(|e| AppError::Internal(e.to_string()))?;
|
||||
Ok(axum::response::Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header(header::CONTENT_TYPE, "application/json")
|
||||
.body(Body::from(body))
|
||||
.unwrap())
|
||||
}
|
||||
ExportPayload::Csv(bytes) => {
|
||||
Ok(axum::response::Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header(header::CONTENT_TYPE, "text/csv; charset=utf-8")
|
||||
.header(header::CONTENT_DISPOSITION, format!("attachment; filename=\"{}.csv\"", filename))
|
||||
.body(Body::from(bytes))
|
||||
.unwrap())
|
||||
}
|
||||
ExportPayload::Xlsx(bytes) => {
|
||||
Ok(axum::response::Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header(header::CONTENT_TYPE, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
|
||||
.header(header::CONTENT_DISPOSITION, format!("attachment; filename=\"{}.xlsx\"", filename))
|
||||
.body(Body::from(bytes))
|
||||
.unwrap())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -873,3 +905,33 @@ where
|
||||
|
||||
Ok(Json(ApiResponse::ok(result)))
|
||||
}
|
||||
|
||||
/// POST /api/v1/plugins/{plugin_id}/reconcile — 对账扫描
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/plugins/{plugin_id}/reconcile",
|
||||
responses(
|
||||
(status = 200, description = "对账报告", body = ApiResponse<ReconciliationReport>)
|
||||
),
|
||||
tag = "Plugin Data",
|
||||
)]
|
||||
pub async fn reconcile_refs<S>(
|
||||
State(state): State<PluginState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Path(plugin_id): Path<Uuid>,
|
||||
) -> Result<Json<ApiResponse<crate::data_dto::ReconciliationReport>>, AppError>
|
||||
where
|
||||
PluginState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "plugin.admin")?;
|
||||
|
||||
let report = PluginDataService::reconcile_references(
|
||||
plugin_id,
|
||||
ctx.tenant_id,
|
||||
&state.db,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(Json(ApiResponse::ok(report)))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user