fix: 修复测试发现的 7 个问题 + 全 workspace clippy 清零
功能修复: 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:
@@ -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()),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user