feat(plugin): SQL 构建支持行级数据范围条件

DynamicTableManager 新增 build_data_scope_condition_with_params 方法,
支持 all/self/department/department_tree 四种数据范围过滤。
部门成员为空时自动退化为 self 范围,支持 Generated Column 路由。
附带 6 个单元测试覆盖所有场景。
This commit is contained in:
iven
2026-04-17 10:36:01 +08:00
parent 649334e862
commit dadb826804

View File

@@ -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<Value>) {
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<String> = dept_member_ids
.iter()
.enumerate()
.map(|(i, _)| format!("${}", start_param_idx + i))
.collect();
let values: Vec<Value> = 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
);
}
}