diff --git a/crates/erp-plugin/src/data_service.rs b/crates/erp-plugin/src/data_service.rs index f84eb76..3053461 100644 --- a/crates/erp-plugin/src/data_service.rs +++ b/crates/erp-plugin/src/data_service.rs @@ -209,6 +209,13 @@ impl PluginDataService { validate_data(&data, &fields)?; validate_ref_entities(&data, &fields, entity_name, plugin_id, tenant_id, db, false, Some(id)).await?; + // 循环引用检测 + for field in &fields { + if field.no_cycle == Some(true) && data.get(&field.name).is_some() { + check_no_cycle(id, field, &data, &info.table_name, tenant_id, db).await?; + } + } + let (sql, values) = DynamicTableManager::build_update_sql( &info.table_name, id, @@ -576,6 +583,62 @@ async fn validate_ref_entities( Ok(()) } +/// 循环引用检测 — 用于 no_cycle 字段 +async fn check_no_cycle( + record_id: Uuid, + field: &PluginField, + data: &serde_json::Value, + table_name: &str, + tenant_id: Uuid, + db: &sea_orm::DatabaseConnection, +) -> AppResult<()> { + let Some(val) = data.get(&field.name) else { return Ok(()) }; + let new_parent = val.as_str().unwrap_or("").trim().to_string(); + if new_parent.is_empty() { return Ok(()); } + + let new_parent_id = Uuid::parse_str(&new_parent).map_err(|_| { + AppError::Validation("parent_id 不是有效的 UUID".to_string()) + })?; + + let field_name = sanitize_identifier(&field.name); + let mut visited = vec![record_id]; + let mut current_id = new_parent_id; + + for _ in 0..100 { + if visited.contains(¤t_id) { + let label = field.display_name.as_deref().unwrap_or(&field.name); + return Err(AppError::Validation(format!( + "字段 '{}' 形成循环引用", label + ))); + } + visited.push(current_id); + + let query_sql = format!( + "SELECT data->>'{}' as parent FROM \"{}\" WHERE id = $1 AND tenant_id = $2 AND deleted_at IS NULL", + field_name, table_name + ); + #[derive(FromQueryResult)] + struct ParentRow { parent: Option } + let row = ParentRow::find_by_statement(Statement::from_sql_and_values( + sea_orm::DatabaseBackend::Postgres, + query_sql, + [current_id.into(), tenant_id.into()], + )).one(db).await?; + + match row { + Some(r) => { + let parent = r.parent.unwrap_or_default().trim().to_string(); + if parent.is_empty() { break; } + current_id = Uuid::parse_str(&parent).map_err(|_| { + AppError::Internal("parent_id 不是有效的 UUID".to_string()) + })?; + } + None => break, + } + } + Ok(()) +} + #[cfg(test)] mod validate_tests { use super::*;