feat(plugin): SQL 构建支持行级数据范围条件
DynamicTableManager 新增 build_data_scope_condition_with_params 方法, 支持 all/self/department/department_tree 四种数据范围过滤。 部门成员为空时自动退化为 self 范围,支持 Generated Column 路由。 附带 6 个单元测试覆盖所有场景。
This commit is contained in:
@@ -600,6 +600,68 @@ impl DynamicTableManager {
|
|||||||
Ok((sql, values))
|
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 {
|
pub fn encode_cursor(values: &[String], id: &Uuid) -> String {
|
||||||
let obj = serde_json::json!({
|
let obj = serde_json::json!({
|
||||||
@@ -1124,4 +1186,134 @@ mod tests {
|
|||||||
"应有 tenant_id + cursor_val + cursor_id + limit"
|
"应有 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
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user