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

@@ -3,7 +3,10 @@ use std::panic::AssertUnwindSafe;
use std::sync::Arc;
use dashmap::DashMap;
use sea_orm::{ColumnTrait, ConnectionTrait, DatabaseConnection, EntityTrait, QueryFilter, Statement, TransactionTrait};
use sea_orm::{
ColumnTrait, ConnectionTrait, DatabaseConnection, EntityTrait, QueryFilter, Statement,
TransactionTrait,
};
use serde_json::json;
use tokio::sync::RwLock;
use uuid::Uuid;
@@ -190,9 +193,11 @@ impl PluginEngine {
let result = self
.execute_wasm(plugin_id, &ctx, |store, instance| {
instance.erp_plugin_plugin_api().call_init(store)
instance
.erp_plugin_plugin_api()
.call_init(store)
.map_err(|e| PluginError::ExecutionError(e.to_string()))?
.map_err(|e| PluginError::ExecutionError(e))?;
.map_err(PluginError::ExecutionError)?;
Ok(())
})
.await;
@@ -296,7 +301,7 @@ impl PluginEngine {
.erp_plugin_plugin_api()
.call_handle_event(store, &event_type, &payload_bytes)
.map_err(|e| PluginError::ExecutionError(e.to_string()))?
.map_err(|e| PluginError::ExecutionError(e))?;
.map_err(PluginError::ExecutionError)?;
Ok(())
})
.await
@@ -317,7 +322,7 @@ impl PluginEngine {
.erp_plugin_plugin_api()
.call_on_tenant_created(store, &tenant_id_str)
.map_err(|e| PluginError::ExecutionError(e.to_string()))?
.map_err(|e| PluginError::ExecutionError(e))?;
.map_err(PluginError::ExecutionError)?;
Ok(())
})
.await
@@ -351,7 +356,9 @@ impl PluginEngine {
/// 将插件从一个 key 重命名为另一个 key用于热更新的原子替换
pub async fn rename_plugin(&self, old_id: &str, new_id: &str) -> PluginResult<()> {
let (_, loaded) = self.plugins.remove(old_id)
let (_, loaded) = self
.plugins
.remove(old_id)
.ok_or_else(|| PluginError::NotFound(old_id.to_string()))?;
let mut loaded = Arc::try_unwrap(loaded)
.map_err(|_| PluginError::ExecutionError("插件仍被引用,无法重命名".to_string()))?;
@@ -419,7 +426,10 @@ impl PluginEngine {
if entry.value().id == plugin_id {
// 配置会在下次 execute_wasm 时从数据库自动重新加载
// 这里只清理可能缓存的旧配置
tracing::info!(plugin_id, "Plugin config refresh scheduled (loaded on next invocation)");
tracing::info!(
plugin_id,
"Plugin config refresh scheduled (loaded on next invocation)"
);
return Ok(());
}
}
@@ -438,12 +448,9 @@ impl PluginEngine {
/// 恢复数据库中状态为 running/enabled 的插件。
///
/// 服务器重启后调用此方法,重新加载 WASM 到内存并启动事件监听。
pub async fn recover_plugins(
&self,
db: &DatabaseConnection,
) -> PluginResult<Vec<String>> {
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
pub async fn recover_plugins(&self, db: &DatabaseConnection) -> PluginResult<Vec<String>> {
use crate::entity::plugin;
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
// 查询所有运行中的插件
let running_plugins = plugin::Entity::find()
@@ -472,7 +479,10 @@ impl PluginEngine {
}
// 加载 WASM 到内存
if let Err(e) = self.load(plugin_id_str, &model.wasm_binary, manifest.clone()).await {
if let Err(e) = self
.load(plugin_id_str, &model.wasm_binary, manifest.clone())
.await
{
tracing::error!(
plugin_id = %plugin_id_str,
tenant_id = %tenant_id,
@@ -543,7 +553,8 @@ impl PluginEngine {
let loaded = self.get_loaded(plugin_id)?;
// 构建跨插件实体映射(从 manifest 的 ref_plugin 字段提取)
let cross_plugin_entities = Self::build_cross_plugin_map(&loaded.manifest, &self.db, exec_ctx.tenant_id).await;
let cross_plugin_entities =
Self::build_cross_plugin_map(&loaded.manifest, &self.db, exec_ctx.tenant_id).await;
// 加载插件配置(从数据库)
let plugin_config = Self::load_plugin_config(plugin_id, exec_ctx.tenant_id, &self.db).await;
@@ -569,43 +580,41 @@ impl PluginEngine {
store.limiter(|state| &mut state.limits);
// 实例化
let instance = PluginWorld::instantiate_async(&mut store, &loaded.component, &loaded.linker)
.await
.map_err(|e| PluginError::InstantiationError(e.to_string()))?;
let instance =
PluginWorld::instantiate_async(&mut store, &loaded.component, &loaded.linker)
.await
.map_err(|e| PluginError::InstantiationError(e.to_string()))?;
let timeout_secs = self.config.execution_timeout_secs;
let pid_owned = plugin_id.to_owned();
let start = std::time::Instant::now();
// spawn_blocking 闭包执行 WASM正常完成时收集 pending_ops
let (result, pending_ops): (PluginResult<R>, Vec<PendingOp>) =
tokio::time::timeout(
std::time::Duration::from_secs(timeout_secs),
tokio::task::spawn_blocking(move || {
match std::panic::catch_unwind(AssertUnwindSafe(|| {
let r = operation(&mut store, &instance);
// catch_unwind 内部不能调用 into_data需要 &mut self
// 但这里 operation 已完成store 仍可用
let ops = std::mem::take(&mut store.data_mut().pending_ops);
(r, ops)
})) {
Ok((r, ops)) => (r, ops),
Err(_) => {
// panic 后丢弃所有 pending_ops,避免半完成状态写入数据库
tracing::warn!(plugin = %pid_owned, "WASM panic, discarding pending ops");
(
Err(PluginError::ExecutionError("WASM panic".to_string())),
Vec::new(),
)
}
let (result, pending_ops): (PluginResult<R>, Vec<PendingOp>) = tokio::time::timeout(
std::time::Duration::from_secs(timeout_secs),
tokio::task::spawn_blocking(move || {
match std::panic::catch_unwind(AssertUnwindSafe(|| {
let r = operation(&mut store, &instance);
// catch_unwind 内部不能调用 into_data需要 &mut self
// 但这里 operation 已完成store 仍可用
let ops = std::mem::take(&mut store.data_mut().pending_ops);
(r, ops)
})) {
Ok((r, ops)) => (r, ops),
Err(_) => {
// panic 后丢弃所有 pending_ops避免半完成状态写入数据库
tracing::warn!(plugin = %pid_owned, "WASM panic, discarding pending ops");
(
Err(PluginError::ExecutionError("WASM panic".to_string())),
Vec::new(),
)
}
}),
)
.await
.map_err(|_| {
PluginError::ExecutionError(format!("插件执行超时 ({}s)", timeout_secs))
})?
.map_err(|e| PluginError::ExecutionError(e.to_string()))?;
}
}),
)
.await
.map_err(|_| PluginError::ExecutionError(format!("插件执行超时 ({}s)", timeout_secs)))?
.map_err(|e| PluginError::ExecutionError(e.to_string()))?;
// 更新运行时指标
let elapsed_ms = start.elapsed().as_millis() as f64;
@@ -639,13 +648,16 @@ impl PluginEngine {
plugin_id: &str,
tenant_id: Uuid,
db: &DatabaseConnection,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = serde_json::Value> + Send + 'static>> {
) -> std::pin::Pin<Box<dyn std::future::Future<Output = serde_json::Value> + Send + 'static>>
{
let db = db.clone();
let pid = plugin_id.to_string();
Box::pin(async move {
use sea_orm::FromQueryResult;
#[derive(Debug, FromQueryResult)]
struct ConfigRow { config_json: serde_json::Value }
struct ConfigRow {
config_json: serde_json::Value,
}
ConfigRow::find_by_statement(Statement::from_sql_and_values(
sea_orm::DatabaseBackend::Postgres,
"SELECT config_json FROM plugins WHERE tenant_id = $1\n\
@@ -671,16 +683,26 @@ impl PluginEngine {
tenant_id: Uuid,
) -> HashMap<String, String> {
let mut map = HashMap::new();
let Some(schema) = &manifest.schema else { return map };
let Some(schema) = &manifest.schema else {
return map;
};
for entity in &schema.entities {
for field in &entity.fields {
if let (Some(target_plugin), Some(ref_entity)) = (&field.ref_plugin, &field.ref_entity) {
if let (Some(target_plugin), Some(ref_entity)) =
(&field.ref_plugin, &field.ref_entity)
{
let key = format!("{}.{}", target_plugin, ref_entity);
// 从 plugin_entities 表查找目标表名
let table_name = crate::entity::plugin_entity::Entity::find()
.filter(crate::entity::plugin_entity::Column::ManifestId.eq(target_plugin.as_str()))
.filter(crate::entity::plugin_entity::Column::EntityName.eq(ref_entity.as_str()))
.filter(
crate::entity::plugin_entity::Column::ManifestId
.eq(target_plugin.as_str()),
)
.filter(
crate::entity::plugin_entity::Column::EntityName
.eq(ref_entity.as_str()),
)
.filter(crate::entity::plugin_entity::Column::TenantId.eq(tenant_id))
.filter(crate::entity::plugin_entity::Column::DeletedAt.is_null())
.one(db)
@@ -716,7 +738,10 @@ impl PluginEngine {
}
// 使用事务确保所有数据库操作的原子性
let txn = db.begin().await.map_err(|e| PluginError::DatabaseError(e.to_string()))?;
let txn = db
.begin()
.await
.map_err(|e| PluginError::DatabaseError(e.to_string()))?;
for op in &ops {
match op {
@@ -724,11 +749,16 @@ impl PluginEngine {
let table_name = DynamicTableManager::table_name(plugin_id, entity);
let parsed_data: serde_json::Value =
serde_json::from_slice(data).unwrap_or_default();
let id_uuid = id.parse::<Uuid>().map_err(|e| {
PluginError::ExecutionError(format!("无效的 ID: {}", e))
})?;
let (sql, values) =
DynamicTableManager::build_insert_sql_with_id(&table_name, id_uuid, tenant_id, user_id, &parsed_data);
let id_uuid = id
.parse::<Uuid>()
.map_err(|e| PluginError::ExecutionError(format!("无效的 ID: {}", e)))?;
let (sql, values) = DynamicTableManager::build_insert_sql_with_id(
&table_name,
id_uuid,
tenant_id,
user_id,
&parsed_data,
);
txn.execute(Statement::from_sql_and_values(
sea_orm::DatabaseBackend::Postgres,
sql,
@@ -752,9 +782,9 @@ impl PluginEngine {
let table_name = DynamicTableManager::table_name(plugin_id, entity);
let parsed_data: serde_json::Value =
serde_json::from_slice(data).unwrap_or_default();
let id_uuid = id.parse::<Uuid>().map_err(|e| {
PluginError::ExecutionError(format!("无效的 ID: {}", e))
})?;
let id_uuid = id
.parse::<Uuid>()
.map_err(|e| PluginError::ExecutionError(format!("无效的 ID: {}", e)))?;
let (sql, values) = DynamicTableManager::build_update_sql(
&table_name,
id_uuid,
@@ -780,9 +810,9 @@ impl PluginEngine {
}
PendingOp::Delete { entity, id } => {
let table_name = DynamicTableManager::table_name(plugin_id, entity);
let id_uuid = id.parse::<Uuid>().map_err(|e| {
PluginError::ExecutionError(format!("无效的 ID: {}", e))
})?;
let id_uuid = id
.parse::<Uuid>()
.map_err(|e| PluginError::ExecutionError(format!("无效的 ID: {}", e)))?;
let (sql, values) =
DynamicTableManager::build_delete_sql(&table_name, id_uuid, tenant_id);
txn.execute(Statement::from_sql_and_values(
@@ -807,18 +837,21 @@ impl PluginEngine {
}
// 提交事务
txn.commit().await.map_err(|e| PluginError::DatabaseError(e.to_string()))?;
txn.commit()
.await
.map_err(|e| PluginError::DatabaseError(e.to_string()))?;
// 事务提交成功后发布事件best-effort不阻塞主流程
for op in ops {
if let PendingOp::PublishEvent { event_type, payload } = op {
if let PendingOp::PublishEvent {
event_type,
payload,
} = op
{
let parsed_payload: serde_json::Value =
serde_json::from_slice(&payload).unwrap_or_default();
let event = erp_core::events::DomainEvent::new(
&event_type,
tenant_id,
parsed_payload,
);
let event =
erp_core::events::DomainEvent::new(&event_type, tenant_id, parsed_payload);
event_bus.publish(event, db).await;
tracing::debug!(