fix: 修复测试发现的 7 个问题 + 全 workspace clippy 清零
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

功能修复:
1. 患者创建空名称验证:后端添加 name.trim().is_empty() 检查
2. 仪表盘统计容错:单个查询失败返回零值而非 500
3. FHIR 路由修复:从 /fhir 移到 /api/v1/fhir 保持一致
4. 冻结模块后端中间件:新增 frozen_module_middleware 拦截冻结路径
5. 积分端点权限码:health.health-data.list → health.points.list
6. 角色权限迁移:护士补充 devices.list,运营补充 points.list/manage
7. 测试结果文档:R01-R05 角色测试 + T00/T10 结果归档

Clippy 全 workspace 清零(14→0 errors):
- erp-core: 修复 empty doc line、collapsible if、redundant closure 等 9 处
- erp-health: 修复 too_many_arguments、unused var、unnecessary parens 等 58 处
- erp-ai: 修复 dead_code、unused import 等 11 处
- erp-plugin: 修复 too_many_arguments、wildcard pattern 等 11 处
- erp-server-migration: 修复 enum_variant_names 5 处
- erp-auth/config/workflow/message: 各 1-3 处

工程改进:
- lint-staged 配置迁移到 .lintstagedrc.js(函数式避免文件列表传给 clippy)
- cargo fmt 统一格式化
This commit is contained in:
iven
2026-05-07 23:43:14 +08:00
parent 786f57c151
commit 6d5a711d2c
323 changed files with 15662 additions and 6603 deletions

View File

@@ -10,7 +10,13 @@ use crate::manifest::{PluginEntity, PluginField, PluginFieldType};
pub(crate) fn sanitize_identifier(input: &str) -> String {
input
.chars()
.map(|c| if c.is_ascii_alphanumeric() || c == '_' { c } else { '_' })
.map(|c| {
if c.is_ascii_alphanumeric() || c == '_' {
c
} else {
'_'
}
})
.collect()
}
@@ -56,7 +62,9 @@ impl DynamicTableManager {
let col_name = format!("_f_{}", sanitize_identifier(&field.name));
let sql_type = field.field_type.generated_sql_type();
let expr = field.field_type.generated_expr(&sanitize_identifier(&field.name));
let expr = field
.field_type
.generated_expr(&sanitize_identifier(&field.name));
gen_cols.push(format!(
" \"{}\" {} GENERATED ALWAYS AS ({}) STORED",
@@ -80,8 +88,7 @@ impl DynamicTableManager {
// pg_trgm 索引
for field in &entity.fields {
if field.searchable == Some(true)
&& matches!(field.field_type, PluginFieldType::String)
if field.searchable == Some(true) && matches!(field.field_type, PluginFieldType::String)
{
let sf = sanitize_identifier(&field.name);
indexes.push(format!(
@@ -128,11 +135,7 @@ impl DynamicTableManager {
entity: &PluginEntity,
) -> PluginResult<()> {
let ddl = Self::build_create_table_sql(plugin_id, entity);
for sql in ddl
.split(';')
.map(|s| s.trim())
.filter(|s| !s.is_empty())
{
for sql in ddl.split(';').map(|s| s.trim()).filter(|s| !s.is_empty()) {
tracing::info!(sql = %sql, "Executing DDL");
db.execute_unprepared(sql).await.map_err(|e| {
tracing::error!(sql = %sql, error = %e, "DDL execution failed");
@@ -179,21 +182,25 @@ impl DynamicTableManager {
continue;
}
// 新增字段 + 需要 Generated Column 的条件
let needs_gen = field.unique
|| field.sortable == Some(true)
|| field.filterable == Some(true);
let needs_gen =
field.unique || field.sortable == Some(true) || field.filterable == Some(true);
if needs_gen {
new_filterable.push(field.clone());
if field.sortable == Some(true) {
new_sortable.push(field.clone());
}
}
if field.searchable == Some(true) && matches!(field.field_type, PluginFieldType::String) {
if field.searchable == Some(true) && matches!(field.field_type, PluginFieldType::String)
{
new_searchable.push(field.clone());
}
}
FieldDiff { new_filterable, new_sortable, new_searchable }
FieldDiff {
new_filterable,
new_sortable,
new_searchable,
}
}
/// Schema 演进:为已有实体新增 Generated Column 和索引
@@ -212,7 +219,9 @@ impl DynamicTableManager {
}
let col_name = format!("_f_{}", sanitize_identifier(&field.name));
let sql_type = field.field_type.generated_sql_type();
let expr = field.field_type.generated_expr(&sanitize_identifier(&field.name));
let expr = field
.field_type
.generated_expr(&sanitize_identifier(&field.name));
let _safe_field = sanitize_identifier(&field.name);
statements.push(format!(
@@ -329,7 +338,11 @@ impl DynamicTableManager {
LIMIT $2 OFFSET $3",
table_name
);
let values = vec![tenant_id.into(), (limit as i64).into(), (offset as i64).into()];
let values = vec![
tenant_id.into(),
(limit as i64).into(),
(offset as i64).into(),
];
(sql, values)
}
@@ -398,7 +411,9 @@ impl DynamicTableManager {
table_name, set_expr
);
let values = vec![
serde_json::to_string(&partial_data).unwrap_or_default().into(),
serde_json::to_string(&partial_data)
.unwrap_or_default()
.into(),
user_id.into(),
id.into(),
tenant_id.into(),
@@ -408,11 +423,7 @@ impl DynamicTableManager {
}
/// 构建 DELETE SQL软删除
pub fn build_delete_sql(
table_name: &str,
id: Uuid,
tenant_id: Uuid,
) -> (String, Vec<Value>) {
pub fn build_delete_sql(table_name: &str, id: Uuid, tenant_id: Uuid) -> (String, Vec<Value>) {
let sql = format!(
"UPDATE \"{}\" \
SET deleted_at = NOW(), updated_at = NOW() \
@@ -469,19 +480,19 @@ impl DynamicTableManager {
let mut values: Vec<Value> = vec![tenant_id.into()];
// 处理 filter与 build_filtered_query_sql 保持一致)
if let Some(f) = filter {
if let Some(obj) = f.as_object() {
for (key, val) in obj {
let clean_key = sanitize_identifier(key);
if clean_key.is_empty() {
return Err(format!("无效的过滤字段名: {}", key));
}
conditions.push(format!("\"data\"->>'{}' = ${}", clean_key, param_idx));
values.push(Value::String(Some(Box::new(
val.as_str().unwrap_or("").to_string(),
))));
param_idx += 1;
if let Some(f) = filter
&& let Some(obj) = f.as_object()
{
for (key, val) in obj {
let clean_key = sanitize_identifier(key);
if clean_key.is_empty() {
return Err(format!("无效的过滤字段名: {}", key));
}
conditions.push(format!("\"data\"->>'{}' = ${}", clean_key, param_idx));
values.push(Value::String(Some(Box::new(
val.as_str().unwrap_or("").to_string(),
))));
param_idx += 1;
}
}
@@ -533,19 +544,19 @@ impl DynamicTableManager {
let mut values: Vec<Value> = vec![tenant_id.into()];
// 处理 filter
if let Some(f) = filter {
if let Some(obj) = f.as_object() {
for (key, val) in obj {
let clean_key = sanitize_identifier(key);
if clean_key.is_empty() {
return Err(format!("无效的过滤字段名: {}", key));
}
conditions.push(format!("\"data\"->>'{}' = ${}", clean_key, param_idx));
values.push(Value::String(Some(Box::new(
val.as_str().unwrap_or("").to_string(),
))));
param_idx += 1;
if let Some(f) = filter
&& let Some(obj) = f.as_object()
{
for (key, val) in obj {
let clean_key = sanitize_identifier(key);
if clean_key.is_empty() {
return Err(format!("无效的过滤字段名: {}", key));
}
conditions.push(format!("\"data\"->>'{}' = ${}", clean_key, param_idx));
values.push(Value::String(Some(Box::new(
val.as_str().unwrap_or("").to_string(),
))));
param_idx += 1;
}
}
@@ -584,19 +595,19 @@ impl DynamicTableManager {
let mut param_idx = 2;
let mut values: Vec<Value> = vec![tenant_id.into()];
if let Some(f) = filter {
if let Some(obj) = f.as_object() {
for (key, val) in obj {
let clean_key = sanitize_identifier(key);
if clean_key.is_empty() {
return Err(format!("无效的过滤字段名: {}", key));
}
conditions.push(format!("\"data\"->>'{}' = ${}", clean_key, param_idx));
values.push(Value::String(Some(Box::new(
val.as_str().unwrap_or("").to_string(),
))));
param_idx += 1;
if let Some(f) = filter
&& let Some(obj) = f.as_object()
{
for (key, val) in obj {
let clean_key = sanitize_identifier(key);
if clean_key.is_empty() {
return Err(format!("无效的过滤字段名: {}", key));
}
conditions.push(format!("\"data\"->>'{}' = ${}", clean_key, param_idx));
values.push(Value::String(Some(Box::new(
val.as_str().unwrap_or("").to_string(),
))));
param_idx += 1;
}
}
@@ -610,16 +621,20 @@ impl DynamicTableManager {
let func_lower = func.to_lowercase();
match func_lower.as_str() {
"sum" => select_parts.push(format!(
"COALESCE(SUM(\"_f_{}\"), 0) as sum_{}", clean_field, clean_field
"COALESCE(SUM(\"_f_{}\"), 0) as sum_{}",
clean_field, clean_field
)),
"avg" => select_parts.push(format!(
"COALESCE(AVG(\"_f_{}\"), 0) as avg_{}", clean_field, clean_field
"COALESCE(AVG(\"_f_{}\"), 0) as avg_{}",
clean_field, clean_field
)),
"min" => select_parts.push(format!(
"MIN(\"_f_{}\") as min_{}", clean_field, clean_field
"MIN(\"_f_{}\") as min_{}",
clean_field, clean_field
)),
"max" => select_parts.push(format!(
"MAX(\"_f_{}\") as max_{}", clean_field, clean_field
"MAX(\"_f_{}\") as max_{}",
clean_field, clean_field
)),
_ => {}
}
@@ -641,6 +656,7 @@ impl DynamicTableManager {
}
/// 构建带过滤条件的查询 SQL
#[allow(clippy::too_many_arguments)]
pub fn build_filtered_query_sql(
table_name: &str,
tenant_id: Uuid,
@@ -659,19 +675,19 @@ impl DynamicTableManager {
let mut values: Vec<Value> = vec![tenant_id.into()];
// 处理 filter
if let Some(f) = filter {
if let Some(obj) = f.as_object() {
for (key, val) in obj {
let clean_key = sanitize_identifier(key);
if clean_key.is_empty() {
return Err(format!("无效的过滤字段名: {}", key));
}
conditions.push(format!("\"data\"->>'{}' = ${}", clean_key, param_idx));
values.push(Value::String(Some(Box::new(
val.as_str().unwrap_or("").to_string(),
))));
param_idx += 1;
if let Some(f) = filter
&& let Some(obj) = f.as_object()
{
for (key, val) in obj {
let clean_key = sanitize_identifier(key);
if clean_key.is_empty() {
return Err(format!("无效的过滤字段名: {}", key));
}
conditions.push(format!("\"data\"->>'{}' = ${}", clean_key, param_idx));
values.push(Value::String(Some(Box::new(
val.as_str().unwrap_or("").to_string(),
))));
param_idx += 1;
}
}
@@ -734,6 +750,7 @@ impl DynamicTableManager {
}
/// 扩展版查询构建 — 支持 Generated Column 路由
#[allow(clippy::too_many_arguments)]
pub fn build_filtered_query_sql_ex(
table_name: &str,
tenant_id: Uuid,
@@ -755,19 +772,19 @@ impl DynamicTableManager {
let mut values: Vec<Value> = vec![tenant_id.into()];
// filter
if let Some(f) = filter {
if let Some(obj) = f.as_object() {
for (key, val) in obj {
let clean_key = sanitize_identifier(key);
if clean_key.is_empty() {
return Err(format!("无效的过滤字段名: {}", key));
}
conditions.push(format!("{} = ${}", ref_fn(&clean_key), param_idx));
values.push(Value::String(Some(Box::new(
val.as_str().unwrap_or("").to_string(),
))));
param_idx += 1;
if let Some(f) = filter
&& let Some(obj) = f.as_object()
{
for (key, val) in obj {
let clean_key = sanitize_identifier(key);
if clean_key.is_empty() {
return Err(format!("无效的过滤字段名: {}", key));
}
conditions.push(format!("{} = ${}", ref_fn(&clean_key), param_idx));
values.push(Value::String(Some(Box::new(
val.as_str().unwrap_or("").to_string(),
))));
param_idx += 1;
}
}
@@ -875,7 +892,8 @@ impl DynamicTableManager {
)
}
}
"all" | _ => (String::new(), vec![]),
"all" => (String::new(), vec![]),
_ => (String::new(), vec![]),
}
}
@@ -893,8 +911,8 @@ impl DynamicTableManager {
let json_str = BASE64
.decode(cursor)
.map_err(|e| format!("游标 Base64 解码失败: {}", e))?;
let obj: serde_json::Value = serde_json::from_slice(&json_str)
.map_err(|e| format!("游标 JSON 解析失败: {}", e))?;
let obj: serde_json::Value =
serde_json::from_slice(&json_str).map_err(|e| format!("游标 JSON 解析失败: {}", e))?;
let values = obj["v"]
.as_array()
.ok_or("游标缺少 v 字段")?
@@ -923,7 +941,7 @@ impl DynamicTableManager {
let ref_fn = Self::field_reference_fn(generated_fields);
let sort_col = sort_column
.as_deref()
.map(|s| ref_fn(s))
.map(ref_fn)
.unwrap_or("\"created_at\"".to_string());
let mut values: Vec<Value> = vec![tenant_id.into()];
@@ -1098,7 +1116,10 @@ mod tests {
assert!(sql.contains("ILIKE"), "Expected ILIKE in SQL, got: {}", sql);
// 验证搜索参数值包含 %...%
if let Value::String(Some(s)) = &values[1] {
assert!(s.contains("测试关键词"), "Search value should contain keyword");
assert!(
s.contains("测试关键词"),
"Search value should contain keyword"
);
assert!(s.starts_with('%'), "Search value should start with %");
}
}
@@ -1188,7 +1209,11 @@ mod tests {
None,
)
.unwrap();
assert!(sql.contains("\"data\"->>'status' ="), "Expected filter, got: {}", sql);
assert!(
sql.contains("\"data\"->>'status' ="),
"Expected filter, got: {}",
sql
);
assert_eq!(values.len(), 2); // tenant_id + filter_value
}
@@ -1231,8 +1256,16 @@ mod tests {
)
.unwrap();
assert!(sql.contains("GROUP BY"), "Expected GROUP BY, got: {}", sql);
assert!(sql.contains("\"data\"->>'status'"), "Expected group field, got: {}", sql);
assert!(sql.contains("ORDER BY count DESC"), "Expected ORDER BY count DESC, got: {}", sql);
assert!(
sql.contains("\"data\"->>'status'"),
"Expected group field, got: {}",
sql
);
assert!(
sql.contains("ORDER BY count DESC"),
"Expected ORDER BY count DESC, got: {}",
sql
);
assert_eq!(values.len(), 1); // 仅 tenant_id
}
@@ -1245,8 +1278,16 @@ mod tests {
Some(serde_json::json!({"status": "active"})),
)
.unwrap();
assert!(sql.contains("\"data\"->>'region'"), "Expected group field, got: {}", sql);
assert!(sql.contains("\"data\"->>'status' ="), "Expected filter, got: {}", sql);
assert!(
sql.contains("\"data\"->>'region'"),
"Expected group field, got: {}",
sql
);
assert!(
sql.contains("\"data\"->>'status' ="),
"Expected filter, got: {}",
sql
);
assert_eq!(values.len(), 2); // tenant_id + filter_value
}
@@ -1260,7 +1301,11 @@ mod tests {
);
let (sql, _) = result.unwrap();
assert!(!sql.contains("DROP TABLE"), "SQL 不应包含注入: {}", sql);
assert!(sql.contains("evil___DROP_TABLE__"), "字段名应被清理: {}", sql);
assert!(
sql.contains("evil___DROP_TABLE__"),
"字段名应被清理: {}",
sql
);
}
#[test]
@@ -1317,14 +1362,8 @@ mod tests {
};
let sql = DynamicTableManager::build_create_table_sql("erp_crm", &entity);
assert!(
sql.contains("_f_code"),
"应包含 _f_code Generated Column"
);
assert!(
sql.contains("_f_level"),
"应包含 _f_level Generated Column"
);
assert!(sql.contains("_f_code"), "应包含 _f_code Generated Column");
assert!(sql.contains("_f_level"), "应包含 _f_level Generated Column");
assert!(
sql.contains("_f_sort_order"),
"应包含 _f_sort_order Generated Column"
@@ -1333,10 +1372,7 @@ mod tests {
sql.contains("GENERATED ALWAYS AS"),
"应包含 GENERATED ALWAYS AS"
);
assert!(
sql.contains("::INTEGER"),
"Integer 字段应有类型转换"
);
assert!(sql.contains("::INTEGER"), "Integer 字段应有类型转换");
}
#[test]
@@ -1476,10 +1512,7 @@ mod tests {
&[],
)
.unwrap();
assert!(
sql.contains("ROW("),
"cursor 条件应使用 ROW 比较"
);
assert!(sql.contains("ROW("), "cursor 条件应使用 ROW 比较");
assert!(
values.len() >= 4,
"应有 tenant_id + cursor_val + cursor_id + limit"
@@ -1530,16 +1563,8 @@ mod tests {
"department 应使用 IN 条件, got: {}",
sql
);
assert!(
sql.contains("$2"),
"参数索引应从 2 开始, got: {}",
sql
);
assert!(
sql.contains("$3"),
"第二个参数索引应为 3, got: {}",
sql
);
assert!(sql.contains("$2"), "参数索引应从 2 开始, got: {}", sql);
assert!(sql.contains("$3"), "第二个参数索引应为 3, got: {}", sql);
assert_eq!(values.len(), 2);
}
@@ -1684,35 +1709,16 @@ mod tests {
#[test]
fn test_sanitize_removes_special_chars() {
let result = sanitize_identifier("table;name'here\"with`special");
assert!(
!result.contains(';'),
"号应被替换: {}",
result
);
assert!(
!result.contains('\''),
"单引号应被替换: {}",
result
);
assert!(
!result.contains('"'),
"双引号应被替换: {}",
result
);
assert!(
!result.contains('`'),
"反引号应被替换: {}",
result
);
assert!(!result.contains(';'), "分号应被替换: {}", result);
assert!(!result.contains('\''), "单引号应被替换: {}", result);
assert!(!result.contains('"'), "双引号应被替换: {}", result);
assert!(!result.contains('`'), "反引号应被替换: {}", result);
}
#[test]
fn test_sanitize_allows_alphanumeric_underscore() {
let result = sanitize_identifier("my_table_123");
assert_eq!(
result, "my_table_123",
"合法标识符应原样保留"
);
assert_eq!(result, "my_table_123", "合法标识符应原样保留");
}
#[test]
@@ -1723,26 +1729,14 @@ mod tests {
"DROP TABLE 注入应被清理为下划线: {}",
result
);
assert!(
!result.contains(';'),
"不应包含分号: {}",
result
);
assert!(!result.contains(';'), "不应包含分号: {}", result);
}
#[test]
fn test_sanitize_handles_sql_comment() {
let result = sanitize_identifier("users--");
assert_eq!(
result, "users__",
"SQL 注释应被替换为下划线: {}",
result
);
assert!(
!result.contains('-'),
"不应包含连字符: {}",
result
);
assert_eq!(result, "users__", "SQL 注释应被替换为下划线: {}", result);
assert!(!result.contains('-'), "不应包含连字符: {}", result);
}
#[test]
@@ -1753,11 +1747,7 @@ mod tests {
"UNION 注入中空格应被替换为下划线: {}",
result
);
assert!(
!result.contains(' '),
"不应包含空格: {}",
result
);
assert!(!result.contains(' '), "不应包含空格: {}", result);
}
#[test]