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

@@ -1,21 +1,21 @@
use chrono::Utc;
use sea_orm::{ActiveModelTrait, ColumnTrait, ConnectionTrait, EntityTrait, PaginatorTrait, QueryFilter, Set};
use sea_orm::{
ActiveModelTrait, ColumnTrait, ConnectionTrait, EntityTrait, PaginatorTrait, QueryFilter, Set,
};
use sha2::{Digest, Sha256};
use std::collections::HashMap;
use uuid::Uuid;
use sha2::{Sha256, Digest};
use erp_core::sea_orm_ext::bump_version;
use erp_core::error::AppResult;
use crate::dto::{
PluginEntityResp, PluginHealthResp, PluginPermissionResp, PluginResp,
};
use crate::dto::{PluginEntityResp, PluginHealthResp, PluginPermissionResp, PluginResp};
use crate::dynamic_table::DynamicTableManager;
use crate::engine::PluginEngine;
use crate::entity::{plugin, plugin_entity, plugin_event_subscription};
use crate::error::PluginError;
use crate::manifest::{parse_manifest, PluginManifest};
use crate::manifest::{PluginManifest, parse_manifest};
pub struct PluginService;
@@ -32,11 +32,14 @@ impl PluginService {
let manifest = parse_manifest(manifest_toml)?;
// 安全扫描
let validation = crate::plugin_validator::validate_plugin_security(&manifest, wasm_binary.len())?;
let validation =
crate::plugin_validator::validate_plugin_security(&manifest, wasm_binary.len())?;
if !validation.valid {
return Err(PluginError::ValidationError(format!(
"插件安全校验失败: {}", validation.errors.join("; ")
)).into());
"插件安全校验失败: {}",
validation.errors.join("; ")
))
.into());
}
// 计算 WASM hash
@@ -48,8 +51,8 @@ impl PluginService {
let plugin_id = Uuid::now_v7();
// 序列化 manifest 为 JSON
let manifest_json =
serde_json::to_value(&manifest).map_err(|e| PluginError::InvalidManifest(e.to_string()))?;
let manifest_json = serde_json::to_value(&manifest)
.map_err(|e| PluginError::InvalidManifest(e.to_string()))?;
let model = plugin::ActiveModel {
id: Set(plugin_id),
@@ -98,9 +101,8 @@ impl PluginService {
let model = find_plugin(plugin_id, tenant_id, db).await?;
validate_status(&model.status, "uploaded")?;
let manifest: PluginManifest =
serde_json::from_value(model.manifest_json.clone())
.map_err(|e| PluginError::InvalidManifest(e.to_string()))?;
let manifest: PluginManifest = serde_json::from_value(model.manifest_json.clone())
.map_err(|e| PluginError::InvalidManifest(e.to_string()))?;
let now = Utc::now();
@@ -108,7 +110,8 @@ impl PluginService {
let mut entity_resps = Vec::new();
if let Some(schema) = &manifest.schema {
for (i, entity_def) in schema.entities.iter().enumerate() {
let table_name = DynamicTableManager::table_name(&manifest.metadata.id, &entity_def.name);
let table_name =
DynamicTableManager::table_name(&manifest.metadata.id, &entity_def.name);
tracing::info!(step = i, entity = %entity_def.name, table = %table_name, "Creating dynamic table");
// 创建动态表
@@ -185,11 +188,7 @@ impl PluginService {
// 加载到内存
tracing::info!(manifest_id = %manifest.metadata.id, "Loading plugin into engine");
engine
.load(
&manifest.metadata.id,
&model.wasm_binary,
manifest.clone(),
)
.load(&manifest.metadata.id, &model.wasm_binary, manifest.clone())
.await?;
// 更新状态
@@ -214,9 +213,8 @@ impl PluginService {
let model = find_plugin(plugin_id, tenant_id, db).await?;
validate_status_any(&model.status, &["installed", "disabled"])?;
let manifest: PluginManifest =
serde_json::from_value(model.manifest_json.clone())
.map_err(|e| PluginError::InvalidManifest(e.to_string()))?;
let manifest: PluginManifest = serde_json::from_value(model.manifest_json.clone())
.map_err(|e| PluginError::InvalidManifest(e.to_string()))?;
let plugin_manifest_id = &manifest.metadata.id;
@@ -270,9 +268,8 @@ impl PluginService {
let model = find_plugin(plugin_id, tenant_id, db).await?;
validate_status_any(&model.status, &["running", "enabled"])?;
let manifest: PluginManifest =
serde_json::from_value(model.manifest_json.clone())
.map_err(|e| PluginError::InvalidManifest(e.to_string()))?;
let manifest: PluginManifest = serde_json::from_value(model.manifest_json.clone())
.map_err(|e| PluginError::InvalidManifest(e.to_string()))?;
// 禁用引擎
engine.disable(&manifest.metadata.id).await?;
@@ -299,9 +296,8 @@ impl PluginService {
let model = find_plugin(plugin_id, tenant_id, db).await?;
validate_status_any(&model.status, &["installed", "disabled"])?;
let manifest: PluginManifest =
serde_json::from_value(model.manifest_json.clone())
.map_err(|e| PluginError::InvalidManifest(e.to_string()))?;
let manifest: PluginManifest = serde_json::from_value(model.manifest_json.clone())
.map_err(|e| PluginError::InvalidManifest(e.to_string()))?;
// 卸载(如果 disabled 状态engine 可能仍在内存中)
engine.unload(&manifest.metadata.id).await.ok();
@@ -376,19 +372,16 @@ impl PluginService {
}
if let Some(q) = search {
query = query.filter(
plugin::Column::Name.contains(q)
plugin::Column::Name
.contains(q)
.or(plugin::Column::Description.contains(q)),
);
}
let paginator = query
.clone()
.paginate(db, page_size);
let paginator = query.clone().paginate(db, page_size);
let total = paginator.num_items().await?;
let models = paginator
.fetch_page(page.saturating_sub(1))
.await?;
let models = paginator.fetch_page(page.saturating_sub(1)).await?;
let mut resps = Vec::with_capacity(models.len());
@@ -397,27 +390,25 @@ impl PluginService {
let entities_map = find_batch_plugin_entities(&plugin_ids, tenant_id, db).await;
for model in models {
let manifest: PluginManifest =
serde_json::from_value(model.manifest_json.clone()).unwrap_or_else(|_| {
PluginManifest {
metadata: crate::manifest::PluginMetadata {
id: String::new(),
name: String::new(),
version: String::new(),
description: String::new(),
author: String::new(),
min_platform_version: None,
dependencies: vec![],
},
schema: None,
events: None,
ui: None,
permissions: None,
settings: None,
numbering: None,
templates: None,
trigger_events: None,
}
let manifest: PluginManifest = serde_json::from_value(model.manifest_json.clone())
.unwrap_or_else(|_| PluginManifest {
metadata: crate::manifest::PluginMetadata {
id: String::new(),
name: String::new(),
version: String::new(),
description: String::new(),
author: String::new(),
min_platform_version: None,
dependencies: vec![],
},
schema: None,
events: None,
ui: None,
permissions: None,
settings: None,
numbering: None,
templates: None,
trigger_events: None,
});
let entities = entities_map.get(&model.id).cloned().unwrap_or_default();
resps.push(plugin_model_to_resp(&model, &manifest, entities));
@@ -433,9 +424,8 @@ impl PluginService {
db: &sea_orm::DatabaseConnection,
) -> AppResult<PluginResp> {
let model = find_plugin(plugin_id, tenant_id, db).await?;
let manifest: PluginManifest =
serde_json::from_value(model.manifest_json.clone())
.map_err(|e| PluginError::InvalidManifest(e.to_string()))?;
let manifest: PluginManifest = serde_json::from_value(model.manifest_json.clone())
.map_err(|e| PluginError::InvalidManifest(e.to_string()))?;
let entities = find_plugin_entities(plugin_id, tenant_id, db).await?;
Ok(plugin_model_to_resp(&model, &manifest, entities))
}
@@ -455,13 +445,15 @@ impl PluginService {
erp_core::error::check_version(expected_version, model.version)?;
// 校验配置值是否符合 manifest settings 声明
let manifest: PluginManifest =
serde_json::from_value(model.manifest_json.clone())
.map_err(|e| PluginError::InvalidManifest(e.to_string()))?;
let manifest: PluginManifest = serde_json::from_value(model.manifest_json.clone())
.map_err(|e| PluginError::InvalidManifest(e.to_string()))?;
if let Some(settings) = &manifest.settings {
validate_plugin_settings(config.as_object().ok_or_else(|| {
PluginError::ValidationError("config 必须是 JSON 对象".to_string())
})?, &settings.fields)?;
validate_plugin_settings(
config.as_object().ok_or_else(|| {
PluginError::ValidationError("config 必须是 JSON 对象".to_string())
})?,
&settings.fields,
)?;
}
let now = Utc::now();
@@ -485,7 +477,9 @@ impl PluginService {
bus.publish(event, db).await;
}
let entities = find_plugin_entities(plugin_id, tenant_id, db).await.unwrap_or_default();
let entities = find_plugin_entities(plugin_id, tenant_id, db)
.await
.unwrap_or_default();
Ok(plugin_model_to_resp(&model, &manifest, entities))
}
@@ -497,9 +491,8 @@ impl PluginService {
engine: &PluginEngine,
) -> AppResult<PluginHealthResp> {
let model = find_plugin(plugin_id, tenant_id, db).await?;
let manifest: PluginManifest =
serde_json::from_value(model.manifest_json.clone())
.map_err(|e| PluginError::InvalidManifest(e.to_string()))?;
let manifest: PluginManifest = serde_json::from_value(model.manifest_json.clone())
.map_err(|e| PluginError::InvalidManifest(e.to_string()))?;
let details = engine.health_check(&manifest.metadata.id).await?;
@@ -521,9 +514,8 @@ impl PluginService {
db: &sea_orm::DatabaseConnection,
) -> AppResult<serde_json::Value> {
let model = find_plugin(plugin_id, tenant_id, db).await?;
let manifest: PluginManifest =
serde_json::from_value(model.manifest_json.clone())
.map_err(|e| PluginError::InvalidManifest(e.to_string()))?;
let manifest: PluginManifest = serde_json::from_value(model.manifest_json.clone())
.map_err(|e| PluginError::InvalidManifest(e.to_string()))?;
// 构建 schema 响应entities + ui 页面配置 + settings + numbering + trigger_events
let mut result = serde_json::Map::new();
@@ -599,17 +591,18 @@ impl PluginService {
let new_manifest = parse_manifest(new_manifest_toml)?;
let model = find_plugin(plugin_id, tenant_id, db).await?;
let old_manifest: PluginManifest =
serde_json::from_value(model.manifest_json.clone())
.map_err(|e| PluginError::InvalidManifest(e.to_string()))?;
let old_manifest: PluginManifest = serde_json::from_value(model.manifest_json.clone())
.map_err(|e| PluginError::InvalidManifest(e.to_string()))?;
let old_version = old_manifest.metadata.version.clone();
let new_version = new_manifest.metadata.version.clone();
if old_manifest.metadata.id != new_manifest.metadata.id {
return Err(PluginError::InvalidManifest(
format!("插件 ID 不匹配: 旧={}, 新={}", old_manifest.metadata.id, new_manifest.metadata.id)
).into());
return Err(PluginError::InvalidManifest(format!(
"插件 ID 不匹配: 旧={}, 新={}",
old_manifest.metadata.id, new_manifest.metadata.id
))
.into());
}
let plugin_manifest_id = &new_manifest.metadata.id;
@@ -619,8 +612,8 @@ impl PluginService {
let old_schema = old_manifest.schema.as_ref();
for entity in &new_schema.entities {
let old_entity = old_schema
.and_then(|s| s.entities.iter().find(|e| e.name == entity.name));
let old_entity =
old_schema.and_then(|s| s.entities.iter().find(|e| e.name == entity.name));
match old_entity {
None => {
@@ -637,8 +630,12 @@ impl PluginService {
"Schema 演进:新增 Generated Column"
);
DynamicTableManager::alter_add_generated_columns(
db, plugin_manifest_id, entity, &diff
).await?;
db,
plugin_manifest_id,
entity,
&diff,
)
.await?;
}
}
}
@@ -700,7 +697,10 @@ impl PluginService {
.map_err(|e| PluginError::InvalidManifest(e.to_string()))?);
active.updated_at = Set(now);
active.updated_by = Set(Some(operator_id));
active.update(db).await.map_err(|e| PluginError::DatabaseError(e.to_string()))?;
active
.update(db)
.await
.map_err(|e| PluginError::DatabaseError(e.to_string()))?;
}
}
}
@@ -719,20 +719,16 @@ impl PluginService {
// ---- 内部辅助 ----
fn find_plugin(
async fn find_plugin(
plugin_id: Uuid,
tenant_id: Uuid,
db: &sea_orm::DatabaseConnection,
) -> impl std::future::Future<Output = AppResult<plugin::Model>> + Send {
async move {
plugin::Entity::find_by_id(plugin_id)
.one(db)
.await?
.filter(|m| m.tenant_id == tenant_id && m.deleted_at.is_none())
.ok_or_else(|| {
erp_core::error::AppError::NotFound(format!("插件 {} 不存在", plugin_id))
})
}
) -> AppResult<plugin::Model> {
plugin::Entity::find_by_id(plugin_id)
.one(db)
.await?
.filter(|m| m.tenant_id == tenant_id && m.deleted_at.is_none())
.ok_or_else(|| erp_core::error::AppError::NotFound(format!("插件 {} 不存在", plugin_id)))
}
/// 公开的插件查询 — 供 handler 使用
@@ -764,11 +760,14 @@ async fn find_batch_plugin_entities(
let mut result: HashMap<Uuid, Vec<PluginEntityResp>> = HashMap::new();
for e in entities {
result.entry(e.plugin_id).or_default().push(PluginEntityResp {
name: e.entity_name.clone(),
display_name: e.entity_name,
table_name: e.table_name,
});
result
.entry(e.plugin_id)
.or_default()
.push(PluginEntityResp {
name: e.entity_name.clone(),
display_name: e.entity_name,
table_name: e.table_name,
});
}
result
}
@@ -849,39 +848,38 @@ fn validate_plugin_settings(
}
// 类型校验
if let Some(val) = value {
if !val.is_null() {
let type_ok = match field.field_type {
PluginSettingType::Text => val.is_string(),
PluginSettingType::Number => val.is_number(),
PluginSettingType::Boolean => val.is_boolean(),
PluginSettingType::Select => val.is_string(),
PluginSettingType::Multiselect => val.is_array(),
PluginSettingType::Color => val.is_string(),
PluginSettingType::Date => val.is_string(),
PluginSettingType::Datetime => val.is_string(),
PluginSettingType::Json => true,
};
if !type_ok {
return Err(PluginError::ValidationError(format!(
"配置项 '{}' 类型错误,期望 {:?}",
field.name, field.field_type
))
.into());
}
if let Some(val) = value
&& !val.is_null()
{
let type_ok = match field.field_type {
PluginSettingType::Text => val.is_string(),
PluginSettingType::Number => val.is_number(),
PluginSettingType::Boolean => val.is_boolean(),
PluginSettingType::Select => val.is_string(),
PluginSettingType::Multiselect => val.is_array(),
PluginSettingType::Color => val.is_string(),
PluginSettingType::Date => val.is_string(),
PluginSettingType::Datetime => val.is_string(),
PluginSettingType::Json => true,
};
if !type_ok {
return Err(PluginError::ValidationError(format!(
"配置项 '{}' 类型错误,期望 {:?}",
field.name, field.field_type
))
.into());
}
// 数值范围校验
if let Some((min, max)) = field.range {
if let Some(n) = val.as_f64() {
if n < min || n > max {
return Err(PluginError::ValidationError(format!(
"配置项 '{}' ({}) 的值 {} 超出范围 [{}, {}]",
field.name, field.display_name, n, min, max
))
.into());
}
}
}
// 数值范围校验
if let Some((min, max)) = field.range
&& let Some(n) = val.as_f64()
&& (n < min || n > max)
{
return Err(PluginError::ValidationError(format!(
"配置项 '{}' ({}) 的值 {} 超出范围 [{}, {}]",
field.name, field.display_name, n, min, max
))
.into());
}
}
}
@@ -959,7 +957,7 @@ async fn register_plugin_permissions(
sea_orm::Value::from(resource),
sea_orm::Value::from(action),
sea_orm::Value::from(description),
sea_orm::Value::from(now.clone()),
sea_orm::Value::from(*now),
sea_orm::Value::from(operator_id),
],
))
@@ -1038,10 +1036,7 @@ pub async fn grant_permissions_to_admin(
error = %e,
"分配插件权限给 admin 角色失败"
);
PluginError::DatabaseError(format!(
"分配插件权限给 admin 角色失败: {}",
e
))
PluginError::DatabaseError(format!("分配插件权限给 admin 角色失败: {}", e))
})?;
let rows = result.rows_affected();
@@ -1082,7 +1077,7 @@ async fn unregister_plugin_permissions(
sea_orm::DatabaseBackend::Postgres,
rp_sql,
vec![
sea_orm::Value::from(now.clone()),
sea_orm::Value::from(now),
sea_orm::Value::from(tenant_id),
sea_orm::Value::from(prefix.clone()),
],