From dadb826804a8bce22e3756bd0bd128563fba1867 Mon Sep 17 00:00:00 2001 From: iven Date: Fri, 17 Apr 2026 10:36:01 +0800 Subject: [PATCH] =?UTF-8?q?feat(plugin):=20SQL=20=E6=9E=84=E5=BB=BA?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E8=A1=8C=E7=BA=A7=E6=95=B0=E6=8D=AE=E8=8C=83?= =?UTF-8?q?=E5=9B=B4=E6=9D=A1=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DynamicTableManager 新增 build_data_scope_condition_with_params 方法, 支持 all/self/department/department_tree 四种数据范围过滤。 部门成员为空时自动退化为 self 范围,支持 Generated Column 路由。 附带 6 个单元测试覆盖所有场景。 --- crates/erp-plugin/src/dynamic_table.rs | 192 +++++++++++++++++++++++++ 1 file changed, 192 insertions(+) diff --git a/crates/erp-plugin/src/dynamic_table.rs b/crates/erp-plugin/src/dynamic_table.rs index 090b4bd..1dd4bf7 100644 --- a/crates/erp-plugin/src/dynamic_table.rs +++ b/crates/erp-plugin/src/dynamic_table.rs @@ -600,6 +600,68 @@ impl DynamicTableManager { Ok((sql, values)) } + /// 构建数据范围 SQL 条件 — 用于行级数据权限过滤 + /// + /// 根据权限范围级别 (scope_level) 生成对应的 WHERE 子句和参数: + /// - "all": 无额外条件(空字符串) + /// - "self": 只能看自己创建/拥有的数据 + /// - "department" / "department_tree": 能看部门成员的数据 + /// + /// 返回 (sql_fragment, params),sql_fragment 可直接拼接到 WHERE 子句中 + pub fn build_data_scope_condition_with_params( + scope_level: &str, + current_user_id: &Uuid, + owner_field: &str, + dept_member_ids: &[Uuid], + start_param_idx: usize, + generated_fields: &[String], + ) -> (String, Vec) { + let ref_fn = Self::field_reference_fn(generated_fields); + let owner_ref = ref_fn(owner_field); + match scope_level { + "self" => ( + format!( + "({} = ${} OR \"created_by\" = ${})", + owner_ref, start_param_idx, start_param_idx + ), + vec![ + current_user_id.to_string().into(), + (*current_user_id).into(), + ], + ), + "department" | "department_tree" => { + if dept_member_ids.is_empty() { + // 部门成员为空时退化为 self 范围 + ( + format!( + "({} = ${} OR \"created_by\" = ${})", + owner_ref, start_param_idx, start_param_idx + ), + vec![ + current_user_id.to_string().into(), + (*current_user_id).into(), + ], + ) + } else { + let placeholders: Vec = dept_member_ids + .iter() + .enumerate() + .map(|(i, _)| format!("${}", start_param_idx + i)) + .collect(); + let values: Vec = dept_member_ids + .iter() + .map(|id| id.to_string().into()) + .collect(); + ( + format!("{} IN ({})", owner_ref, placeholders.join(", ")), + values, + ) + } + } + "all" | _ => (String::new(), vec![]), + } + } + /// 编码游标 pub fn encode_cursor(values: &[String], id: &Uuid) -> String { let obj = serde_json::json!({ @@ -1124,4 +1186,134 @@ mod tests { "应有 tenant_id + cursor_val + cursor_id + limit" ); } + + // ===== build_data_scope_condition_with_params 测试 ===== + + #[test] + fn test_build_data_scope_condition_self() { + let (sql, values) = DynamicTableManager::build_data_scope_condition_with_params( + "self", + &Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap(), + "owner_id", + &[], + 1, + &[], + ); + assert!( + sql.contains("\"data\"->>'owner_id'"), + "self 应包含 owner_id 条件, got: {}", + sql + ); + assert!( + sql.contains("\"created_by\""), + "self 应包含 created_by 条件, got: {}", + sql + ); + assert_eq!(values.len(), 2); + } + + #[test] + fn test_build_data_scope_condition_department() { + let dept_members = vec![ + Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap(), + Uuid::parse_str("00000000-0000-0000-0000-000000000002").unwrap(), + ]; + let (sql, values) = DynamicTableManager::build_data_scope_condition_with_params( + "department", + &Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap(), + "owner_id", + &dept_members, + 2, + &[], + ); + assert!( + sql.contains("IN"), + "department 应使用 IN 条件, got: {}", + sql + ); + assert!( + sql.contains("$2"), + "参数索引应从 2 开始, got: {}", + sql + ); + assert!( + sql.contains("$3"), + "第二个参数索引应为 3, got: {}", + sql + ); + assert_eq!(values.len(), 2); + } + + #[test] + fn test_build_data_scope_condition_department_empty_degrades_to_self() { + let (sql, values) = DynamicTableManager::build_data_scope_condition_with_params( + "department", + &Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap(), + "owner_id", + &[], + 1, + &[], + ); + assert!( + sql.contains("\"created_by\""), + "空部门应退化为 self 范围, got: {}", + sql + ); + assert_eq!(values.len(), 2); + } + + #[test] + fn test_build_data_scope_condition_all() { + let (sql, values) = DynamicTableManager::build_data_scope_condition_with_params( + "all", + &Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap(), + "owner_id", + &[], + 1, + &[], + ); + assert!(sql.is_empty(), "all 应返回空条件"); + assert!(values.is_empty(), "all 应返回空参数"); + } + + #[test] + fn test_build_data_scope_condition_department_tree() { + let dept_members = vec![ + Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap(), + Uuid::parse_str("00000000-0000-0000-0000-000000000002").unwrap(), + Uuid::parse_str("00000000-0000-0000-0000-000000000003").unwrap(), + ]; + let (sql, values) = DynamicTableManager::build_data_scope_condition_with_params( + "department_tree", + &Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap(), + "owner_id", + &dept_members, + 5, + &[], + ); + assert!( + sql.contains("IN"), + "department_tree 应使用 IN 条件, got: {}", + sql + ); + assert_eq!(values.len(), 3); + } + + #[test] + fn test_build_data_scope_condition_with_generated_column() { + let generated = vec!["owner_id".to_string()]; + let (sql, _) = DynamicTableManager::build_data_scope_condition_with_params( + "self", + &Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap(), + "owner_id", + &[], + 1, + &generated, + ); + assert!( + sql.contains("\"_f_owner_id\""), + "generated column 应使用 _f_ 前缀, got: {}", + sql + ); + } }