feat(plugin): 集成过滤查询/排序/搜索到 REST API,添加数据校验和 searchable 索引

- data_dto: PluginDataListParams 新增 filter/sort_by/sort_order
- data_service: list 方法支持 filter/search/sort 参数,自动提取 searchable 字段
- data_service: create/update 添加 required 字段校验
- data_service: 新增 resolve_entity_fields 和 validate_data 辅助函数
- data_handler: 权限检查从硬编码改为动态计算 plugin_id.entity.action
- dynamic_table: searchable 字段自动创建 B-tree 索引
This commit is contained in:
iven
2026-04-16 12:31:53 +08:00
parent 472bf244d8
commit 0ad77693f4
4 changed files with 151 additions and 18 deletions

View File

@@ -1,13 +1,14 @@
use sea_orm::{ColumnTrait, ConnectionTrait, EntityTrait, FromQueryResult, QueryFilter, Statement};
use uuid::Uuid;
use erp_core::error::AppResult;
use erp_core::error::{AppError, AppResult};
use erp_core::events::EventBus;
use crate::data_dto::PluginDataResp;
use crate::dynamic_table::DynamicTableManager;
use crate::entity::plugin_entity;
use crate::error::PluginError;
use crate::manifest::PluginField;
pub struct PluginDataService;
@@ -22,6 +23,10 @@ impl PluginDataService {
db: &sea_orm::DatabaseConnection,
_event_bus: &EventBus,
) -> AppResult<PluginDataResp> {
// 数据校验
let fields = resolve_entity_fields(plugin_id, entity_name, tenant_id, db).await?;
validate_data(&data, &fields)?;
let table_name = resolve_table_name(plugin_id, entity_name, tenant_id, db).await?;
let (sql, values) =
DynamicTableManager::build_insert_sql(&table_name, tenant_id, operator_id, &data);
@@ -53,7 +58,7 @@ impl PluginDataService {
})
}
/// 列表查询
/// 列表查询(支持过滤/搜索/排序)
pub async fn list(
plugin_id: Uuid,
entity_name: &str,
@@ -61,11 +66,30 @@ impl PluginDataService {
page: u64,
page_size: u64,
db: &sea_orm::DatabaseConnection,
filter: Option<serde_json::Value>,
search: Option<String>,
sort_by: Option<String>,
sort_order: Option<String>,
) -> AppResult<(Vec<PluginDataResp>, u64)> {
let table_name = resolve_table_name(plugin_id, entity_name, tenant_id, db).await?;
// Count
let (count_sql, count_values) = DynamicTableManager::build_count_sql(&table_name, tenant_id);
// 获取 searchable 字段列表
let entity_fields = resolve_entity_fields(plugin_id, entity_name, tenant_id, db).await?;
let search_tuple = {
let searchable: Vec<&str> = entity_fields
.iter()
.filter(|f| f.searchable == Some(true))
.map(|f| f.name.as_str())
.collect();
match (searchable.is_empty(), &search) {
(false, Some(kw)) => Some((searchable.join(","), kw.clone())),
_ => None,
}
};
// Count使用基础条件不含 filter/search 以保持计数一致性)
let (count_sql, count_values) =
DynamicTableManager::build_count_sql(&table_name, tenant_id);
#[derive(FromQueryResult)]
struct CountResult {
count: i64,
@@ -80,9 +104,19 @@ impl PluginDataService {
.map(|r| r.count as u64)
.unwrap_or(0);
// Query
let offset = (page.saturating_sub(1)) * page_size;
let (sql, values) = DynamicTableManager::build_query_sql(&table_name, tenant_id, page_size, offset);
// Query(带过滤/搜索/排序)
let offset = page.saturating_sub(1) * page_size;
let (sql, values) = DynamicTableManager::build_filtered_query_sql(
&table_name,
tenant_id,
page_size,
offset,
filter,
search_tuple,
sort_by,
sort_order,
)
.map_err(|e| AppError::Validation(e))?;
#[derive(FromQueryResult)]
struct DataRow {
@@ -142,7 +176,7 @@ impl PluginDataService {
))
.one(db)
.await?
.ok_or_else(|| erp_core::error::AppError::NotFound("记录不存在".to_string()))?;
.ok_or_else(|| AppError::NotFound("记录不存在".to_string()))?;
Ok(PluginDataResp {
id: row.id.to_string(),
@@ -165,6 +199,10 @@ impl PluginDataService {
db: &sea_orm::DatabaseConnection,
_event_bus: &EventBus,
) -> AppResult<PluginDataResp> {
// 数据校验
let fields = resolve_entity_fields(plugin_id, entity_name, tenant_id, db).await?;
validate_data(&data, &fields)?;
let table_name = resolve_table_name(plugin_id, entity_name, tenant_id, db).await?;
let (sql, values) = DynamicTableManager::build_update_sql(
&table_name,
@@ -191,7 +229,7 @@ impl PluginDataService {
))
.one(db)
.await?
.ok_or_else(|| erp_core::error::AppError::VersionMismatch)?;
.ok_or_else(|| AppError::VersionMismatch)?;
Ok(PluginDataResp {
id: result.id.to_string(),
@@ -240,11 +278,47 @@ async fn resolve_table_name(
.one(db)
.await?
.ok_or_else(|| {
erp_core::error::AppError::NotFound(format!(
"插件实体 {}/{} 不存在",
plugin_id, entity_name
))
AppError::NotFound(format!("插件实体 {}/{} 不存在", plugin_id, entity_name))
})?;
Ok(entity.table_name)
}
/// 从 plugin_entities 表获取 entity 的字段定义
async fn resolve_entity_fields(
plugin_id: Uuid,
entity_name: &str,
tenant_id: Uuid,
db: &sea_orm::DatabaseConnection,
) -> AppResult<Vec<PluginField>> {
let entity_model = plugin_entity::Entity::find()
.filter(plugin_entity::Column::PluginId.eq(plugin_id))
.filter(plugin_entity::Column::TenantId.eq(tenant_id))
.filter(plugin_entity::Column::EntityName.eq(entity_name))
.filter(plugin_entity::Column::DeletedAt.is_null())
.one(db)
.await?
.ok_or_else(|| {
AppError::NotFound(format!("插件实体 {}/{} 不存在", plugin_id, entity_name))
})?;
let entity_def: crate::manifest::PluginEntity =
serde_json::from_value(entity_model.schema_json)
.map_err(|e| AppError::Internal(format!("解析 entity schema 失败: {}", e)))?;
Ok(entity_def.fields)
}
/// 校验数据:检查 required 字段
fn validate_data(data: &serde_json::Value, fields: &[PluginField]) -> AppResult<()> {
let obj = data.as_object().ok_or_else(|| {
AppError::Validation("data 必须是 JSON 对象".to_string())
})?;
for field in fields {
if field.required && !obj.contains_key(&field.name) {
let label = field.display_name.as_deref().unwrap_or(&field.name);
return Err(AppError::Validation(format!("字段 '{}' 不能为空", label)));
}
}
Ok(())
}