feat(plugin): 批量操作端点 — batch_delete + batch_update
This commit is contained in:
@@ -72,3 +72,14 @@ pub struct CountQueryParams {
|
|||||||
/// JSON 格式过滤: {"field":"value"}
|
/// JSON 格式过滤: {"field":"value"}
|
||||||
pub filter: Option<String>,
|
pub filter: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 批量操作请求
|
||||||
|
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct BatchActionReq {
|
||||||
|
/// 操作类型: "batch_delete" 或 "batch_update"
|
||||||
|
pub action: String,
|
||||||
|
/// 记录 ID 列表(上限 100)
|
||||||
|
pub ids: Vec<String>,
|
||||||
|
/// batch_update 时的更新数据
|
||||||
|
pub data: Option<serde_json::Value>,
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use uuid::Uuid;
|
|||||||
use erp_core::error::{AppError, AppResult};
|
use erp_core::error::{AppError, AppResult};
|
||||||
use erp_core::events::EventBus;
|
use erp_core::events::EventBus;
|
||||||
|
|
||||||
use crate::data_dto::PluginDataResp;
|
use crate::data_dto::{BatchActionReq, PluginDataResp};
|
||||||
use crate::dynamic_table::{sanitize_identifier, DynamicTableManager};
|
use crate::dynamic_table::{sanitize_identifier, DynamicTableManager};
|
||||||
use crate::entity::plugin;
|
use crate::entity::plugin;
|
||||||
use crate::entity::plugin_entity;
|
use crate::entity::plugin_entity;
|
||||||
@@ -372,6 +372,111 @@ impl PluginDataService {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 批量操作 — batch_delete / batch_update
|
||||||
|
pub async fn batch(
|
||||||
|
plugin_id: Uuid,
|
||||||
|
entity_name: &str,
|
||||||
|
tenant_id: Uuid,
|
||||||
|
operator_id: Uuid,
|
||||||
|
req: BatchActionReq,
|
||||||
|
db: &sea_orm::DatabaseConnection,
|
||||||
|
) -> AppResult<u64> {
|
||||||
|
if req.ids.is_empty() {
|
||||||
|
return Err(AppError::Validation("ids 不能为空".to_string()));
|
||||||
|
}
|
||||||
|
if req.ids.len() > 100 {
|
||||||
|
return Err(AppError::Validation("批量操作上限 100 条".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let info = resolve_entity_info(plugin_id, entity_name, tenant_id, db).await?;
|
||||||
|
let ids: Vec<Uuid> = req
|
||||||
|
.ids
|
||||||
|
.iter()
|
||||||
|
.map(|s| Uuid::parse_str(s))
|
||||||
|
.collect::<Result<Vec<_>, _>>()
|
||||||
|
.map_err(|_| AppError::Validation("ids 中包含无效的 UUID".to_string()))?;
|
||||||
|
|
||||||
|
let affected = match req.action.as_str() {
|
||||||
|
"batch_delete" => {
|
||||||
|
let placeholders: Vec<String> = ids
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, _)| format!("${}", i + 2))
|
||||||
|
.collect();
|
||||||
|
let sql = format!(
|
||||||
|
"UPDATE \"{}\" SET deleted_at = NOW(), updated_at = NOW() \
|
||||||
|
WHERE tenant_id = $1 AND id IN ({}) AND deleted_at IS NULL",
|
||||||
|
info.table_name,
|
||||||
|
placeholders.join(", ")
|
||||||
|
);
|
||||||
|
let mut values = vec![tenant_id.into()];
|
||||||
|
for id in &ids {
|
||||||
|
values.push((*id).into());
|
||||||
|
}
|
||||||
|
let result = db
|
||||||
|
.execute(Statement::from_sql_and_values(
|
||||||
|
sea_orm::DatabaseBackend::Postgres,
|
||||||
|
sql,
|
||||||
|
values,
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
|
result.rows_affected()
|
||||||
|
}
|
||||||
|
"batch_update" => {
|
||||||
|
let update_data = req.data.ok_or_else(|| {
|
||||||
|
AppError::Validation("batch_update 需要 data 字段".to_string())
|
||||||
|
})?;
|
||||||
|
let mut set_expr = "data".to_string();
|
||||||
|
if let Some(obj) = update_data.as_object() {
|
||||||
|
for key in obj.keys() {
|
||||||
|
let clean_key = sanitize_identifier(key);
|
||||||
|
set_expr = format!(
|
||||||
|
"jsonb_set({}, '{{{}}}', $2::jsonb->'{}', true)",
|
||||||
|
set_expr, clean_key, clean_key
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let placeholders: Vec<String> = ids
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, _)| format!("${}", i + 3))
|
||||||
|
.collect();
|
||||||
|
let sql = format!(
|
||||||
|
"UPDATE \"{}\" SET data = {}, updated_at = NOW(), updated_by = $1, version = version + 1 \
|
||||||
|
WHERE tenant_id = $1 AND id IN ({}) AND deleted_at IS NULL",
|
||||||
|
info.table_name,
|
||||||
|
set_expr,
|
||||||
|
placeholders.join(", ")
|
||||||
|
);
|
||||||
|
let mut values = vec![operator_id.into()];
|
||||||
|
values.push(
|
||||||
|
serde_json::to_string(&update_data)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
for id in &ids {
|
||||||
|
values.push((*id).into());
|
||||||
|
}
|
||||||
|
let result = db
|
||||||
|
.execute(Statement::from_sql_and_values(
|
||||||
|
sea_orm::DatabaseBackend::Postgres,
|
||||||
|
sql,
|
||||||
|
values,
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
|
result.rows_affected()
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return Err(AppError::Validation(format!(
|
||||||
|
"不支持的批量操作: {}",
|
||||||
|
req.action
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(affected)
|
||||||
|
}
|
||||||
|
|
||||||
/// 统计记录数(支持过滤和搜索)
|
/// 统计记录数(支持过滤和搜索)
|
||||||
pub async fn count(
|
pub async fn count(
|
||||||
plugin_id: Uuid,
|
plugin_id: Uuid,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use erp_core::rbac::require_permission;
|
|||||||
use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
|
use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
|
||||||
|
|
||||||
use crate::data_dto::{
|
use crate::data_dto::{
|
||||||
AggregateItem, AggregateQueryParams, CountQueryParams, CreatePluginDataReq,
|
AggregateItem, AggregateQueryParams, BatchActionReq, CountQueryParams, CreatePluginDataReq,
|
||||||
PatchPluginDataReq, PluginDataListParams, PluginDataResp, UpdatePluginDataReq,
|
PatchPluginDataReq, PluginDataListParams, PluginDataResp, UpdatePluginDataReq,
|
||||||
};
|
};
|
||||||
use crate::data_service::{PluginDataService, resolve_manifest_id};
|
use crate::data_service::{PluginDataService, resolve_manifest_id};
|
||||||
@@ -322,6 +322,49 @@ where
|
|||||||
Ok(Json(ApiResponse::ok(())))
|
Ok(Json(ApiResponse::ok(())))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/api/v1/plugins/{plugin_id}/{entity}/batch",
|
||||||
|
request_body = BatchActionReq,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "批量操作成功", body = ApiResponse<u64>),
|
||||||
|
),
|
||||||
|
security(("bearer_auth" = [])),
|
||||||
|
tag = "插件数据"
|
||||||
|
)]
|
||||||
|
/// POST /api/v1/plugins/{plugin_id}/{entity}/batch — 批量操作 (batch_delete / batch_update)
|
||||||
|
pub async fn batch_plugin_data<S>(
|
||||||
|
State(state): State<PluginState>,
|
||||||
|
Extension(ctx): Extension<TenantContext>,
|
||||||
|
Path((plugin_id, entity)): Path<(Uuid, String)>,
|
||||||
|
Json(req): Json<BatchActionReq>,
|
||||||
|
) -> Result<Json<ApiResponse<u64>>, 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 action_perm = match req.action.as_str() {
|
||||||
|
"batch_delete" => "delete",
|
||||||
|
"batch_update" => "update",
|
||||||
|
_ => "update",
|
||||||
|
};
|
||||||
|
let fine_perm = compute_permission_code(&manifest_id, &entity, action_perm);
|
||||||
|
require_permission(&ctx, &fine_perm)?;
|
||||||
|
|
||||||
|
let affected = PluginDataService::batch(
|
||||||
|
plugin_id,
|
||||||
|
&entity,
|
||||||
|
ctx.tenant_id,
|
||||||
|
ctx.user_id,
|
||||||
|
req,
|
||||||
|
&state.db,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(Json(ApiResponse::ok(affected)))
|
||||||
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
get,
|
get,
|
||||||
path = "/api/v1/plugins/{plugin_id}/{entity}/count",
|
path = "/api/v1/plugins/{plugin_id}/{entity}/count",
|
||||||
|
|||||||
@@ -86,6 +86,11 @@ impl PluginModule {
|
|||||||
.route(
|
.route(
|
||||||
"/plugins/{plugin_id}/{entity}/aggregate",
|
"/plugins/{plugin_id}/{entity}/aggregate",
|
||||||
get(crate::handler::data_handler::aggregate_plugin_data::<S>),
|
get(crate::handler::data_handler::aggregate_plugin_data::<S>),
|
||||||
|
)
|
||||||
|
// 批量操作路由
|
||||||
|
.route(
|
||||||
|
"/plugins/{plugin_id}/{entity}/batch",
|
||||||
|
post(crate::handler::data_handler::batch_plugin_data::<S>),
|
||||||
);
|
);
|
||||||
|
|
||||||
admin_routes.merge(data_routes)
|
admin_routes.merge(data_routes)
|
||||||
|
|||||||
Reference in New Issue
Block a user