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:
@@ -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!(
|
||||
|
||||
Reference in New Issue
Block a user