feat(plugin): P1-P4 审计修复 — 第三批 (配置变更通知 + 自定义视图)

3.1 配置变更通知:
- update_config 增加 EventBus 参数
- 更新成功后发布 plugin.config.updated 事件
- handler 传入 event_bus

3.2 自定义视图:
- plugin_user_views 表迁移 (id/tenant_id/user_id/plugin_id/entity/view_name/view_config/is_default)
- CRUD API: GET/POST /plugins/{id}/{entity}/views, DELETE /plugins/{id}/{entity}/views/{view_id}
- 默认视图互斥逻辑
This commit is contained in:
iven
2026-04-19 18:25:03 +08:00
parent 0a041c3d22
commit a7a48167ca
8 changed files with 253 additions and 2 deletions

View File

@@ -12,8 +12,9 @@ use crate::data_dto::{
CountQueryParams, CreatePluginDataReq, ExportParams, ImportReq, ImportResult,
PatchPluginDataReq, PluginDataListParams,
PluginDataResp, PublicEntityResp, ReconciliationReport, ResolveLabelsReq, ResolveLabelsResp,
TimeseriesItem, TimeseriesParams, UpdatePluginDataReq,
TimeseriesItem, TimeseriesParams, UpdatePluginDataReq, UserViewReq, UserViewResp,
};
use sea_orm::{ConnectionTrait, Statement};
use crate::data_service::{DataScopeParams, PluginDataService, resolve_manifest_id};
use crate::state::PluginState;
@@ -935,3 +936,142 @@ where
Ok(Json(ApiResponse::ok(report)))
}
// ─── 用户自定义视图 CRUD ──────────────────────────────────────────────────
#[utoipa::path(
get,
path = "/api/v1/plugins/{plugin_id}/{entity}/views",
responses(
(status = 200, description = "视图列表", body = ApiResponse<Vec<UserViewResp>>)
),
tag = "Plugin Views",
)]
pub async fn list_user_views<S>(
State(state): State<PluginState>,
Extension(ctx): Extension<TenantContext>,
Path((plugin_id, entity)): Path<(Uuid, String)>,
) -> Result<Json<ApiResponse<Vec<crate::data_dto::UserViewResp>>>, AppError>
where
PluginState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
let manifest_id = resolve_manifest_id(plugin_id, ctx.tenant_id, &state.db).await?;
use sea_orm::FromQueryResult;
#[derive(FromQueryResult)]
struct ViewRow {
id: Uuid,
view_name: String,
view_config: serde_json::Value,
is_default: bool,
created_at: Option<chrono::DateTime<chrono::Utc>>,
updated_at: Option<chrono::DateTime<chrono::Utc>>,
}
let rows = ViewRow::find_by_statement(Statement::from_sql_and_values(
sea_orm::DatabaseBackend::Postgres,
"SELECT id, view_name, view_config, is_default, created_at, updated_at \
FROM plugin_user_views WHERE tenant_id = $1 AND user_id = $2 AND plugin_id = $3 AND entity_name = $4 \
ORDER BY created_at DESC",
[ctx.tenant_id.into(), ctx.user_id.into(), manifest_id.clone().into(), entity.clone().into()],
))
.all(&state.db)
.await
.map_err(|e| AppError::Internal(e.to_string()))?;
let mid = manifest_id.clone();
let ent = entity.clone();
let items = rows.into_iter().map(|r| UserViewResp {
id: r.id.to_string(),
plugin_id: mid.clone(),
entity_name: ent.clone(),
view_name: r.view_name,
view_config: r.view_config,
is_default: r.is_default,
created_at: r.created_at,
updated_at: r.updated_at,
}).collect();
Ok(Json(ApiResponse::ok(items)))
}
#[utoipa::path(
post,
path = "/api/v1/plugins/{plugin_id}/{entity}/views",
request_body = UserViewReq,
responses(
(status = 200, description = "创建视图", body = ApiResponse<UserViewResp>)
),
tag = "Plugin Views",
)]
pub async fn create_user_view<S>(
State(state): State<PluginState>,
Extension(ctx): Extension<TenantContext>,
Path((plugin_id, entity)): Path<(Uuid, String)>,
Json(req): Json<crate::data_dto::UserViewReq>,
) -> Result<Json<ApiResponse<crate::data_dto::UserViewResp>>, 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 view_id = Uuid::now_v7();
let now = chrono::Utc::now();
let is_default = req.is_default.unwrap_or(false);
let mid = manifest_id.clone();
let ent = entity.clone();
let view_name = req.view_name.clone();
let view_config = req.view_config.clone();
if is_default {
state.db.execute(Statement::from_sql_and_values(
sea_orm::DatabaseBackend::Postgres,
"UPDATE plugin_user_views SET is_default = false \
WHERE tenant_id = $1 AND user_id = $2 AND plugin_id = $3 AND entity_name = $4 AND is_default = true",
[ctx.tenant_id.into(), ctx.user_id.into(), mid.clone().into(), ent.clone().into()],
)).await.map_err(|e| AppError::Internal(e.to_string()))?;
}
state.db.execute(Statement::from_sql_and_values(
sea_orm::DatabaseBackend::Postgres,
"INSERT INTO plugin_user_views (id, tenant_id, user_id, plugin_id, entity_name, view_name, view_config, is_default, created_at, updated_at) \
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)",
[
view_id.into(), ctx.tenant_id.into(), ctx.user_id.into(),
mid.into(), ent.into(),
view_name.into(), view_config.into(), is_default.into(),
now.into(), now.into(),
],
)).await.map_err(|e| AppError::Internal(e.to_string()))?;
Ok(Json(ApiResponse::ok(UserViewResp {
id: view_id.to_string(),
plugin_id: manifest_id,
entity_name: entity,
view_name: req.view_name,
view_config: req.view_config,
is_default,
created_at: Some(now),
updated_at: Some(now),
})))
}
/// DELETE /api/v1/plugins/{plugin_id}/{entity}/views/{view_id} — 删除视图
pub async fn delete_user_view<S>(
State(state): State<PluginState>,
Extension(ctx): Extension<TenantContext>,
Path((plugin_id, entity, view_id)): Path<(Uuid, String, Uuid)>,
) -> Result<Json<ApiResponse<()>>, AppError>
where
PluginState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
state.db.execute(Statement::from_sql_and_values(
sea_orm::DatabaseBackend::Postgres,
"DELETE FROM plugin_user_views WHERE id = $1 AND tenant_id = $2 AND user_id = $3",
[view_id.into(), ctx.tenant_id.into(), ctx.user_id.into()],
)).await.map_err(|e| AppError::Internal(e.to_string()))?;
Ok(Json(ApiResponse::ok(())))
}

View File

@@ -422,6 +422,7 @@ where
req.config,
req.version,
&state.db,
Some(&state.event_bus),
)
.await?;
Ok(Json(ApiResponse::ok(result)))