fix(web,plugin): 前端审计修复 — 401 消除 + 统计卡片 crash + 销售漏斗 500 + antd 6 废弃 API
- API client: proactive token refresh(请求前 30s 检查过期,提前刷新避免 401) - Plugin store: fetchPlugins promise 去重,防止 StrictMode 并发重复请求 - Home stats: 简化 useEffect 加载逻辑,修复 tagColor undefined crash - PluginGraphPage: valueStyle → styles.content, Spin tip → description(antd 6) - DashboardWidgets: trailColor → railColor(antd 6) - data_service: build_scope_sql 参数索引修复(硬编码 $100 → 动态 values.len()+1) - erp-core error: Internal 错误添加 tracing::error 日志输出
This commit is contained in:
@@ -109,14 +109,14 @@ impl PluginDataService {
|
||||
}
|
||||
};
|
||||
|
||||
// 构建数据权限条件
|
||||
let scope_condition = build_scope_sql(&scope, &info.generated_fields);
|
||||
// 构建数据权限条件(count 查询只有 tenant_id 占 $1,scope 从 $2 开始)
|
||||
let count_scope = build_scope_sql(&scope, &info.generated_fields, 2);
|
||||
|
||||
// Count
|
||||
let (count_sql, mut count_values) =
|
||||
DynamicTableManager::build_count_sql(&info.table_name, tenant_id);
|
||||
let count_sql = merge_scope_condition(count_sql, &scope_condition);
|
||||
count_values.extend(scope_condition.1.clone());
|
||||
let count_sql = merge_scope_condition(count_sql, &count_scope);
|
||||
count_values.extend(count_scope.1);
|
||||
|
||||
#[derive(FromQueryResult)]
|
||||
struct CountResult {
|
||||
@@ -147,7 +147,8 @@ impl PluginDataService {
|
||||
)
|
||||
.map_err(|e| AppError::Validation(e))?;
|
||||
|
||||
// 注入数据权限条件
|
||||
// 注入数据权限条件(scope 参数索引接在 values 之后)
|
||||
let scope_condition = build_scope_sql(&scope, &info.generated_fields, values.len() + 1);
|
||||
let sql = merge_scope_condition(sql, &scope_condition);
|
||||
values.extend(scope_condition.1);
|
||||
|
||||
@@ -299,6 +300,22 @@ impl PluginDataService {
|
||||
db: &sea_orm::DatabaseConnection,
|
||||
) -> AppResult<PluginDataResp> {
|
||||
let info = resolve_entity_info(plugin_id, entity_name, tenant_id, db).await?;
|
||||
let fields = info.fields()?;
|
||||
|
||||
// 合并现有数据后校验,确保 partial update 也能触发 required/pattern/ref 校验
|
||||
let existing = Self::get_by_id(plugin_id, entity_name, id, tenant_id, db).await?;
|
||||
let merged = {
|
||||
let mut base = existing.data.as_object().cloned().unwrap_or_default();
|
||||
if let Some(patch) = partial_data.as_object() {
|
||||
for (k, v) in patch {
|
||||
base.insert(k.clone(), v.clone());
|
||||
}
|
||||
}
|
||||
serde_json::Value::Object(base)
|
||||
};
|
||||
|
||||
validate_data(&merged, &fields)?;
|
||||
validate_ref_entities(&merged, &fields, entity_name, plugin_id, tenant_id, db, false, Some(id)).await?;
|
||||
|
||||
let (sql, values) = DynamicTableManager::build_patch_sql(
|
||||
&info.table_name, id, tenant_id, operator_id, partial_data, expected_version,
|
||||
@@ -440,6 +457,62 @@ impl PluginDataService {
|
||||
|
||||
let affected = match req.action.as_str() {
|
||||
"batch_delete" => {
|
||||
// 批量删除前先执行级联策略(逐条,复用 delete 的级联逻辑)
|
||||
let entity_def: crate::manifest::PluginEntity =
|
||||
serde_json::from_value(info.schema_json.clone())
|
||||
.map_err(|e| AppError::Internal(format!("解析 entity schema 失败: {}", e)))?;
|
||||
let manifest_id = resolve_manifest_id(plugin_id, tenant_id, db).await?;
|
||||
|
||||
for &del_id in &ids {
|
||||
for relation in &entity_def.relations {
|
||||
let rel_table = DynamicTableManager::table_name(&manifest_id, &relation.entity);
|
||||
let fk = sanitize_identifier(&relation.foreign_key);
|
||||
match relation.on_delete {
|
||||
crate::manifest::OnDeleteStrategy::Restrict => {
|
||||
let check_sql = format!(
|
||||
"SELECT 1 as chk FROM \"{}\" WHERE data->>'{}' = $1 AND tenant_id = $2 AND deleted_at IS NULL LIMIT 1",
|
||||
rel_table, fk
|
||||
);
|
||||
#[derive(FromQueryResult)]
|
||||
struct RefCheck { chk: Option<i32> }
|
||||
let has_ref = RefCheck::find_by_statement(Statement::from_sql_and_values(
|
||||
sea_orm::DatabaseBackend::Postgres,
|
||||
check_sql,
|
||||
[del_id.to_string().into(), tenant_id.into()],
|
||||
)).one(db).await?;
|
||||
if has_ref.is_some() {
|
||||
return Err(AppError::Validation(format!(
|
||||
"记录 {} 存在关联的 {} 记录,无法删除",
|
||||
del_id, relation.entity
|
||||
)));
|
||||
}
|
||||
}
|
||||
crate::manifest::OnDeleteStrategy::Nullify => {
|
||||
let nullify_sql = format!(
|
||||
"UPDATE \"{}\" SET data = jsonb_set(data, '{{{}}}', 'null'), updated_at = NOW() WHERE data->>'{}' = $1 AND tenant_id = $2 AND deleted_at IS NULL",
|
||||
rel_table, fk, fk
|
||||
);
|
||||
db.execute(Statement::from_sql_and_values(
|
||||
sea_orm::DatabaseBackend::Postgres,
|
||||
nullify_sql,
|
||||
[del_id.to_string().into(), tenant_id.into()],
|
||||
)).await?;
|
||||
}
|
||||
crate::manifest::OnDeleteStrategy::Cascade => {
|
||||
let cascade_sql = format!(
|
||||
"UPDATE \"{}\" SET deleted_at = NOW(), updated_at = NOW() WHERE data->>'{}' = $1 AND tenant_id = $2 AND deleted_at IS NULL",
|
||||
rel_table, fk
|
||||
);
|
||||
db.execute(Statement::from_sql_and_values(
|
||||
sea_orm::DatabaseBackend::Postgres,
|
||||
cascade_sql,
|
||||
[del_id.to_string().into(), tenant_id.into()],
|
||||
)).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let placeholders: Vec<String> = ids
|
||||
.iter()
|
||||
.enumerate()
|
||||
@@ -553,7 +626,7 @@ impl PluginDataService {
|
||||
.map_err(|e| AppError::Validation(e))?;
|
||||
|
||||
// 合并数据权限条件
|
||||
let scope_condition = build_scope_sql(&scope, &info.generated_fields);
|
||||
let scope_condition = build_scope_sql(&scope, &info.generated_fields, values.len() + 1);
|
||||
if !scope_condition.0.is_empty() {
|
||||
sql = merge_scope_condition(sql, &scope_condition);
|
||||
values.extend(scope_condition.1);
|
||||
@@ -599,7 +672,7 @@ impl PluginDataService {
|
||||
.map_err(|e| AppError::Validation(e))?;
|
||||
|
||||
// 合并数据权限条件
|
||||
let scope_condition = build_scope_sql(&scope, &info.generated_fields);
|
||||
let scope_condition = build_scope_sql(&scope, &info.generated_fields, values.len() + 1);
|
||||
if !scope_condition.0.is_empty() {
|
||||
sql = merge_scope_condition(sql, &scope_condition);
|
||||
values.extend(scope_condition.1);
|
||||
@@ -665,7 +738,7 @@ impl PluginDataService {
|
||||
.map_err(|e| AppError::Validation(e))?;
|
||||
|
||||
// 合并数据权限条件
|
||||
let scope_condition = build_scope_sql(&scope, &info.generated_fields);
|
||||
let scope_condition = build_scope_sql(&scope, &info.generated_fields, values.len() + 1);
|
||||
if !scope_condition.0.is_empty() {
|
||||
sql = merge_scope_condition(sql, &scope_condition);
|
||||
values.extend(scope_condition.1);
|
||||
@@ -958,6 +1031,7 @@ async fn check_no_cycle(
|
||||
fn build_scope_sql(
|
||||
scope: &Option<DataScopeParams>,
|
||||
generated_fields: &[String],
|
||||
next_param_idx: usize,
|
||||
) -> (String, Vec<sea_orm::Value>) {
|
||||
match scope {
|
||||
Some(s) => DynamicTableManager::build_data_scope_condition_with_params(
|
||||
@@ -965,7 +1039,7 @@ fn build_scope_sql(
|
||||
&s.user_id,
|
||||
&s.owner_field,
|
||||
&s.dept_member_ids,
|
||||
100, // 起始参数索引(远大于实际参数数量,避免冲突;后续重新编号)
|
||||
next_param_idx,
|
||||
generated_fields,
|
||||
),
|
||||
None => (String::new(), vec![]),
|
||||
|
||||
Reference in New Issue
Block a user