fix(web,plugin): 前端审计修复 — 401 消除 + 统计卡片 crash + 销售漏斗 500 + antd 6 废弃 API
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled

- 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:
iven
2026-04-18 20:31:49 +08:00
parent 790991f77c
commit 5ba11f985f
12 changed files with 308 additions and 100 deletions

View File

@@ -50,7 +50,10 @@ impl IntoResponse for AppError {
AppError::Conflict(_) => (StatusCode::CONFLICT, self.to_string()),
AppError::VersionMismatch => (StatusCode::CONFLICT, self.to_string()),
AppError::TooManyRequests => (StatusCode::TOO_MANY_REQUESTS, self.to_string()),
AppError::Internal(_) => (StatusCode::INTERNAL_SERVER_ERROR, "内部错误".to_string()),
AppError::Internal(msg) => {
tracing::error!("Internal error: {}", msg);
(StatusCode::INTERNAL_SERVER_ERROR, "内部错误".to_string())
}
};
let body = ErrorResponse {

View File

@@ -109,14 +109,14 @@ impl PluginDataService {
}
};
// 构建数据权限条件
let scope_condition = build_scope_sql(&scope, &info.generated_fields);
// 构建数据权限条件count 查询只有 tenant_id 占 $1scope 从 $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![]),