feat(plugin): PATCH 部分更新端点 — jsonb_set 字段合并
This commit is contained in:
@@ -17,13 +17,20 @@ pub struct CreatePluginDataReq {
|
|||||||
pub data: serde_json::Value,
|
pub data: serde_json::Value,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 更新插件数据请求
|
/// 更新插件数据请求(全量替换)
|
||||||
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
|
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
pub struct UpdatePluginDataReq {
|
pub struct UpdatePluginDataReq {
|
||||||
pub data: serde_json::Value,
|
pub data: serde_json::Value,
|
||||||
pub version: i32,
|
pub version: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 部分更新请求(PATCH — 只合并提供的字段)
|
||||||
|
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct PatchPluginDataReq {
|
||||||
|
pub data: serde_json::Value,
|
||||||
|
pub version: i32,
|
||||||
|
}
|
||||||
|
|
||||||
/// 插件数据列表查询参数
|
/// 插件数据列表查询参数
|
||||||
#[derive(Debug, Serialize, Deserialize, utoipa::IntoParams)]
|
#[derive(Debug, Serialize, Deserialize, utoipa::IntoParams)]
|
||||||
pub struct PluginDataListParams {
|
pub struct PluginDataListParams {
|
||||||
|
|||||||
@@ -252,6 +252,45 @@ impl PluginDataService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 部分更新(PATCH)— 只合并提供的字段
|
||||||
|
pub async fn partial_update(
|
||||||
|
plugin_id: Uuid,
|
||||||
|
entity_name: &str,
|
||||||
|
id: Uuid,
|
||||||
|
tenant_id: Uuid,
|
||||||
|
operator_id: Uuid,
|
||||||
|
partial_data: serde_json::Value,
|
||||||
|
expected_version: i32,
|
||||||
|
db: &sea_orm::DatabaseConnection,
|
||||||
|
) -> AppResult<PluginDataResp> {
|
||||||
|
let info = resolve_entity_info(plugin_id, entity_name, tenant_id, db).await?;
|
||||||
|
|
||||||
|
let (sql, values) = DynamicTableManager::build_patch_sql(
|
||||||
|
&info.table_name, id, tenant_id, operator_id, partial_data, expected_version,
|
||||||
|
);
|
||||||
|
|
||||||
|
#[derive(FromQueryResult)]
|
||||||
|
struct PatchResult {
|
||||||
|
id: Uuid,
|
||||||
|
data: serde_json::Value,
|
||||||
|
created_at: chrono::DateTime<chrono::Utc>,
|
||||||
|
updated_at: chrono::DateTime<chrono::Utc>,
|
||||||
|
version: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = PatchResult::find_by_statement(Statement::from_sql_and_values(
|
||||||
|
sea_orm::DatabaseBackend::Postgres, sql, values,
|
||||||
|
)).one(db).await?.ok_or_else(|| AppError::VersionMismatch)?;
|
||||||
|
|
||||||
|
Ok(PluginDataResp {
|
||||||
|
id: result.id.to_string(),
|
||||||
|
data: result.data,
|
||||||
|
created_at: Some(result.created_at),
|
||||||
|
updated_at: Some(result.updated_at),
|
||||||
|
version: Some(result.version),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// 删除(软删除)
|
/// 删除(软删除)
|
||||||
pub async fn delete(
|
pub async fn delete(
|
||||||
plugin_id: Uuid,
|
plugin_id: Uuid,
|
||||||
|
|||||||
@@ -266,6 +266,44 @@ impl DynamicTableManager {
|
|||||||
(sql, values)
|
(sql, values)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 构建 PATCH SQL — 只更新 data 中提供的字段,未提供的保持不变
|
||||||
|
/// 使用 jsonb_set 逐层合并,实现部分更新
|
||||||
|
pub fn build_patch_sql(
|
||||||
|
table_name: &str,
|
||||||
|
id: Uuid,
|
||||||
|
tenant_id: Uuid,
|
||||||
|
user_id: Uuid,
|
||||||
|
partial_data: serde_json::Value,
|
||||||
|
version: i32,
|
||||||
|
) -> (String, Vec<Value>) {
|
||||||
|
let mut set_expr = "data".to_string();
|
||||||
|
if let Some(obj) = partial_data.as_object() {
|
||||||
|
for key in obj.keys() {
|
||||||
|
let clean_key = sanitize_identifier(key);
|
||||||
|
set_expr = format!(
|
||||||
|
"jsonb_set({}, '{{{}}}', $1::jsonb->'{}', true)",
|
||||||
|
set_expr, clean_key, clean_key
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let sql = format!(
|
||||||
|
"UPDATE \"{}\" \
|
||||||
|
SET data = {}, updated_at = NOW(), updated_by = $2, version = version + 1 \
|
||||||
|
WHERE id = $3 AND tenant_id = $4 AND version = $5 AND deleted_at IS NULL \
|
||||||
|
RETURNING id, data, created_at, updated_at, version",
|
||||||
|
table_name, set_expr
|
||||||
|
);
|
||||||
|
let values = vec![
|
||||||
|
serde_json::to_string(&partial_data).unwrap_or_default().into(),
|
||||||
|
user_id.into(),
|
||||||
|
id.into(),
|
||||||
|
tenant_id.into(),
|
||||||
|
version.into(),
|
||||||
|
];
|
||||||
|
(sql, values)
|
||||||
|
}
|
||||||
|
|
||||||
/// 构建 DELETE SQL(软删除)
|
/// 构建 DELETE SQL(软删除)
|
||||||
pub fn build_delete_sql(
|
pub fn build_delete_sql(
|
||||||
table_name: &str,
|
table_name: &str,
|
||||||
@@ -871,6 +909,25 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== build_patch_sql 测试 =====
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_build_patch_sql_merges_fields() {
|
||||||
|
let (sql, values) = DynamicTableManager::build_patch_sql(
|
||||||
|
"plugin_test_customer",
|
||||||
|
Uuid::parse_str("00000000-0000-0000-0000-000000000099").unwrap(),
|
||||||
|
Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap(),
|
||||||
|
Uuid::parse_str("00000000-0000-0000-0000-000000000050").unwrap(),
|
||||||
|
serde_json::json!({"level": "vip", "status": "active"}),
|
||||||
|
3,
|
||||||
|
);
|
||||||
|
assert!(sql.contains("jsonb_set"), "PATCH 应使用 jsonb_set 合并");
|
||||||
|
assert!(sql.contains("version = version + 1"), "PATCH 应更新版本号");
|
||||||
|
assert!(sql.contains("WHERE id = $3"), "应有 id 条件");
|
||||||
|
assert!(sql.contains("version = $5"), "应有乐观锁");
|
||||||
|
assert_eq!(values.len(), 5, "应有 5 个参数");
|
||||||
|
}
|
||||||
|
|
||||||
// ===== build_filtered_count_sql 测试 =====
|
// ===== build_filtered_count_sql 测试 =====
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
|
|||||||
|
|
||||||
use crate::data_dto::{
|
use crate::data_dto::{
|
||||||
AggregateItem, AggregateQueryParams, CountQueryParams, CreatePluginDataReq,
|
AggregateItem, AggregateQueryParams, CountQueryParams, CreatePluginDataReq,
|
||||||
PluginDataListParams, PluginDataResp, UpdatePluginDataReq,
|
PatchPluginDataReq, PluginDataListParams, PluginDataResp, UpdatePluginDataReq,
|
||||||
};
|
};
|
||||||
use crate::data_service::{PluginDataService, resolve_manifest_id};
|
use crate::data_service::{PluginDataService, resolve_manifest_id};
|
||||||
use crate::state::PluginState;
|
use crate::state::PluginState;
|
||||||
@@ -253,6 +253,39 @@ where
|
|||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
patch,
|
||||||
|
path = "/api/v1/plugins/{plugin_id}/{entity}/{id}",
|
||||||
|
request_body = PatchPluginDataReq,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "部分更新成功", body = ApiResponse<PluginDataResp>),
|
||||||
|
),
|
||||||
|
security(("bearer_auth" = [])),
|
||||||
|
tag = "插件数据"
|
||||||
|
)]
|
||||||
|
/// PATCH /api/v1/plugins/{plugin_id}/{entity}/{id} — 部分更新(jsonb_set 合并字段)
|
||||||
|
pub async fn patch_plugin_data<S>(
|
||||||
|
State(state): State<PluginState>,
|
||||||
|
Extension(ctx): Extension<TenantContext>,
|
||||||
|
Path((plugin_id, entity, id)): Path<(Uuid, String, Uuid)>,
|
||||||
|
Json(req): Json<PatchPluginDataReq>,
|
||||||
|
) -> Result<Json<ApiResponse<PluginDataResp>>, 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, "update");
|
||||||
|
require_permission(&ctx, &fine_perm)?;
|
||||||
|
|
||||||
|
let result = PluginDataService::partial_update(
|
||||||
|
plugin_id, &entity, id, ctx.tenant_id, ctx.user_id,
|
||||||
|
req.data, req.version, &state.db,
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
delete,
|
delete,
|
||||||
path = "/api/v1/plugins/{plugin_id}/{entity}/{id}",
|
path = "/api/v1/plugins/{plugin_id}/{entity}/{id}",
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ impl PluginModule {
|
|||||||
"/plugins/{plugin_id}/{entity}/{id}",
|
"/plugins/{plugin_id}/{entity}/{id}",
|
||||||
get(crate::handler::data_handler::get_plugin_data::<S>)
|
get(crate::handler::data_handler::get_plugin_data::<S>)
|
||||||
.put(crate::handler::data_handler::update_plugin_data::<S>)
|
.put(crate::handler::data_handler::update_plugin_data::<S>)
|
||||||
|
.patch(crate::handler::data_handler::patch_plugin_data::<S>)
|
||||||
.delete(crate::handler::data_handler::delete_plugin_data::<S>),
|
.delete(crate::handler::data_handler::delete_plugin_data::<S>),
|
||||||
)
|
)
|
||||||
// 数据统计路由
|
// 数据统计路由
|
||||||
|
|||||||
Reference in New Issue
Block a user