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,13 +10,13 @@ use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
use crate::data_dto::{
AggregateItem, AggregateMultiReq, AggregateMultiRow, AggregateQueryParams, BatchActionReq,
CountQueryParams, CreatePluginDataReq, ExportParams, ImportReq, ImportResult,
PatchPluginDataReq, PluginDataListParams,
PluginDataResp, PublicEntityResp, ReconciliationReport, ResolveLabelsReq, ResolveLabelsResp,
TimeseriesItem, TimeseriesParams, UpdatePluginDataReq, UserViewReq, UserViewResp,
PatchPluginDataReq, PluginDataListParams, PluginDataResp, PublicEntityResp,
ReconciliationReport, ResolveLabelsReq, ResolveLabelsResp, TimeseriesItem, TimeseriesParams,
UpdatePluginDataReq, UserViewReq, UserViewResp,
};
use sea_orm::{ConnectionTrait, Statement};
use crate::data_service::{DataScopeParams, PluginDataService, resolve_manifest_id};
use crate::state::PluginState;
use sea_orm::{ConnectionTrait, Statement};
/// 获取当前用户对指定权限的 data_scope 等级
///
@@ -61,10 +61,7 @@ async fn get_data_scope(
///
/// 当前返回 TenantContext 中的 department_ids。
/// 未来实现递归查询部门树时将支持 include_sub_depts 参数。
async fn get_dept_members(
ctx: &TenantContext,
_include_sub_depts: bool,
) -> Vec<Uuid> {
async fn get_dept_members(ctx: &TenantContext, _include_sub_depts: bool) -> Vec<Uuid> {
// 当前 department_ids 为空时返回空列表
// 未来实现递归查询部门树
if ctx.department_ids.is_empty() {
@@ -109,9 +106,7 @@ where
require_permission(&ctx, &fine_perm)?;
// 解析数据权限范围
let scope = resolve_data_scope(
&ctx, &manifest_id, &entity, &fine_perm, &state.db,
).await?;
let scope = resolve_data_scope(&ctx, &manifest_id, &entity, &fine_perm, &state.db).await?;
let page = params.page.unwrap_or(1);
let page_size = params.page_size.unwrap_or(20);
@@ -282,9 +277,16 @@ where
require_permission(&ctx, &fine_perm)?;
let result = PluginDataService::partial_update(
plugin_id, &entity, id, ctx.tenant_id, ctx.user_id,
req.data, req.version, &state.db,
).await?;
plugin_id,
&entity,
id,
ctx.tenant_id,
ctx.user_id,
req.data,
req.version,
&state.db,
)
.await?;
Ok(Json(ApiResponse::ok(result)))
}
@@ -394,9 +396,7 @@ where
require_permission(&ctx, &fine_perm)?;
// 解析数据权限范围
let scope = resolve_data_scope(
&ctx, &manifest_id, &entity, &fine_perm, &state.db,
).await?;
let scope = resolve_data_scope(&ctx, &manifest_id, &entity, &fine_perm, &state.db).await?;
// 解析 filter JSON
let filter: Option<serde_json::Value> = params
@@ -444,9 +444,7 @@ where
require_permission(&ctx, &fine_perm)?;
// 解析数据权限范围
let scope = resolve_data_scope(
&ctx, &manifest_id, &entity, &fine_perm, &state.db,
).await?;
let scope = resolve_data_scope(&ctx, &manifest_id, &entity, &fine_perm, &state.db).await?;
// 解析 filter JSON
let filter: Option<serde_json::Value> = params
@@ -499,9 +497,7 @@ where
require_permission(&ctx, &fine_perm)?;
// 解析数据权限范围
let scope = resolve_data_scope(
&ctx, &manifest_id, &entity, &fine_perm, &state.db,
).await?;
let scope = resolve_data_scope(&ctx, &manifest_id, &entity, &fine_perm, &state.db).await?;
let result = PluginDataService::timeseries(
plugin_id,
@@ -563,9 +559,8 @@ async fn check_entity_data_scope(
let Some(e) = entity else { return Ok(false) };
let schema: crate::manifest::PluginEntity =
serde_json::from_value(e.schema_json)
.map_err(|e| AppError::Internal(format!("解析 entity schema 失败: {}", e)))?;
let schema: crate::manifest::PluginEntity = serde_json::from_value(e.schema_json)
.map_err(|e| AppError::Internal(format!("解析 entity schema 失败: {}", e)))?;
Ok(schema.data_scope.unwrap_or(false))
}
@@ -595,11 +590,10 @@ where
let fine_perm = compute_permission_code(&manifest_id, &entity, "list");
require_permission(&ctx, &fine_perm)?;
let scope = resolve_data_scope(
&ctx, &manifest_id, &entity, &fine_perm, &state.db,
).await?;
let scope = resolve_data_scope(&ctx, &manifest_id, &entity, &fine_perm, &state.db).await?;
let aggregations: Vec<(String, String)> = body.aggregations
let aggregations: Vec<(String, String)> = body
.aggregations
.iter()
.map(|a| (a.func.clone(), a.field.clone()))
.collect();
@@ -633,9 +627,9 @@ pub async fn resolve_ref_labels<S>(
where
PluginState: FromRef<S>,
{
use sea_orm::{FromQueryResult, Statement};
use crate::data_service::{resolve_cross_plugin_entity, is_plugin_active};
use crate::data_service::{is_plugin_active, resolve_cross_plugin_entity};
use crate::manifest::PluginEntity;
use sea_orm::{FromQueryResult, Statement};
let manifest_id = resolve_manifest_id(plugin_id, ctx.tenant_id, &state.db).await?;
let fine_perm = compute_permission_code(&manifest_id, &entity, "list");
@@ -643,12 +637,15 @@ where
// 获取当前实体的 schema
let entity_info = crate::data_service::resolve_entity_info_cached(
plugin_id, &entity, ctx.tenant_id, &state.db, &state.entity_cache,
).await?;
let entity_def: PluginEntity =
serde_json::from_value(entity_info.schema_json).map_err(|e|
AppError::Internal(format!("解析 entity schema 失败: {}", e))
)?;
plugin_id,
&entity,
ctx.tenant_id,
&state.db,
&state.entity_cache,
)
.await?;
let entity_def: PluginEntity = serde_json::from_value(entity_info.schema_json)
.map_err(|e| AppError::Internal(format!("解析 entity schema 失败: {}", e)))?;
let mut labels = serde_json::Map::<String, serde_json::Value>::new();
let mut meta = serde_json::Map::<String, serde_json::Value>::new();
@@ -657,7 +654,9 @@ where
// 查找字段定义
let field_def = entity_def.fields.iter().find(|f| &f.name == field_name);
let Some(field_def) = field_def else { continue };
let Some(ref_entity_name) = &field_def.ref_entity else { continue };
let Some(ref_entity_name) = &field_def.ref_entity else {
continue;
};
let target_plugin = field_def.ref_plugin.as_deref().unwrap_or(&manifest_id);
let label_field = field_def.ref_label_field.as_deref().unwrap_or("name");
@@ -665,16 +664,20 @@ where
let installed = is_plugin_active(target_plugin, ctx.tenant_id, &state.db).await;
// meta 信息
meta.insert(field_name.clone(), serde_json::json!({
"target_plugin": target_plugin,
"target_entity": ref_entity_name,
"label_field": label_field,
"plugin_installed": installed,
}));
meta.insert(
field_name.clone(),
serde_json::json!({
"target_plugin": target_plugin,
"target_entity": ref_entity_name,
"label_field": label_field,
"plugin_installed": installed,
}),
);
if !installed {
// 目标插件未安装 → 所有 UUID 返回 null
let nulls: serde_json::Map<String, serde_json::Value> = uuids.iter()
let nulls: serde_json::Map<String, serde_json::Value> = uuids
.iter()
.map(|u| (u.clone(), serde_json::Value::Null))
.collect();
labels.insert(field_name.clone(), serde_json::Value::Object(nulls));
@@ -683,10 +686,18 @@ where
// 解析目标表名
let target_table = if field_def.ref_plugin.is_some() {
match resolve_cross_plugin_entity(target_plugin, ref_entity_name, ctx.tenant_id, &state.db).await {
match resolve_cross_plugin_entity(
target_plugin,
ref_entity_name,
ctx.tenant_id,
&state.db,
)
.await
{
Ok(info) => info.table_name,
Err(_) => {
let nulls: serde_json::Map<String, serde_json::Value> = uuids.iter()
let nulls: serde_json::Map<String, serde_json::Value> = uuids
.iter()
.map(|u| (u.clone(), serde_json::Value::Null))
.collect();
labels.insert(field_name.clone(), serde_json::Value::Object(nulls));
@@ -698,33 +709,48 @@ where
};
// 批量查询标签
let uuid_strs: Vec<String> = uuids.iter().filter_map(|u| Uuid::parse_str(u).ok()).map(|u| u.to_string()).collect();
let uuid_strs: Vec<String> = uuids
.iter()
.filter_map(|u| Uuid::parse_str(u).ok())
.map(|u| u.to_string())
.collect();
if uuid_strs.is_empty() {
labels.insert(field_name.clone(), serde_json::json!({}));
continue;
}
// 构建 IN 子句参数
let placeholders: Vec<String> = (2..uuid_strs.len() + 2).map(|i| format!("${}", i)).collect();
let placeholders: Vec<String> = (2..uuid_strs.len() + 2)
.map(|i| format!("${}", i))
.collect();
let sql = format!(
"SELECT id::text, data->>'{}' as label FROM \"{}\" WHERE id IN ({}) AND tenant_id = $1 AND deleted_at IS NULL",
label_field, target_table, placeholders.join(", ")
label_field,
target_table,
placeholders.join(", ")
);
let mut values: Vec<sea_orm::Value> = vec![ctx.tenant_id.into()];
for u in &uuid_strs {
let uuid: Uuid = u.parse().map_err(|e| AppError::Internal(format!("invalid uuid: {}", e)))?;
let uuid: Uuid = u
.parse()
.map_err(|e| AppError::Internal(format!("invalid uuid: {}", e)))?;
values.push(uuid.into());
}
#[derive(FromQueryResult)]
struct LabelRow { id: String, label: Option<String> }
struct LabelRow {
id: String,
label: Option<String>,
}
let rows = LabelRow::find_by_statement(Statement::from_sql_and_values(
sea_orm::DatabaseBackend::Postgres,
sql,
values,
)).all(&state.db).await?;
))
.all(&state.db)
.await?;
let mut field_labels: serde_json::Map<String, serde_json::Value> = serde_json::Map::new();
// 初始化所有请求的 UUID 为 null
@@ -733,7 +759,10 @@ where
}
// 用查询结果填充
for row in rows {
field_labels.insert(row.id, serde_json::Value::String(row.label.unwrap_or_default()));
field_labels.insert(
row.id,
serde_json::Value::String(row.label.unwrap_or_default()),
);
}
labels.insert(field_name.clone(), serde_json::Value::Object(field_labels));
@@ -758,7 +787,7 @@ where
PluginState: FromRef<S>,
{
use crate::entity::plugin_entity;
use sea_orm::{EntityTrait, QueryFilter, ColumnTrait};
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
let entities = plugin_entity::Entity::find()
.filter(plugin_entity::Column::TenantId.eq(ctx.tenant_id))
@@ -767,18 +796,23 @@ where
.all(&state.db)
.await?;
let result: Vec<PublicEntityResp> = entities.iter().map(|e| {
let display_name = e.schema_json.get("display_name")
.and_then(|v| v.as_str())
.unwrap_or(&e.entity_name)
.to_string();
PublicEntityResp {
manifest_id: e.manifest_id.clone(),
plugin_id: e.plugin_id.to_string(),
entity_name: e.entity_name.clone(),
display_name,
}
}).collect();
let result: Vec<PublicEntityResp> = entities
.iter()
.map(|e| {
let display_name = e
.schema_json
.get("display_name")
.and_then(|v| v.as_str())
.unwrap_or(&e.entity_name)
.to_string();
PublicEntityResp {
manifest_id: e.manifest_id.clone(),
plugin_id: e.plugin_id.to_string(),
entity_name: e.entity_name.clone(),
display_name,
}
})
.collect();
Ok(Json(ApiResponse::ok(result)))
}
@@ -807,16 +841,14 @@ where
S: Clone + Send + Sync + 'static,
{
use crate::data_dto::ExportPayload;
use axum::http::{header, StatusCode};
use axum::body::Body;
use axum::http::{StatusCode, header};
let manifest_id = resolve_manifest_id(plugin_id, ctx.tenant_id, &state.db).await?;
let fine_perm = compute_permission_code(&manifest_id, &entity, "list");
require_permission(&ctx, &fine_perm)?;
let scope = resolve_data_scope(
&ctx, &manifest_id, &entity, &fine_perm, &state.db,
).await?;
let scope = resolve_data_scope(&ctx, &manifest_id, &entity, &fine_perm, &state.db).await?;
let filter: Option<serde_json::Value> = params
.filter
@@ -838,7 +870,11 @@ where
)
.await?;
let filename = format!("{}_export_{}", entity, chrono::Utc::now().format("%Y%m%d%H%M%S"));
let filename = format!(
"{}_export_{}",
entity,
chrono::Utc::now().format("%Y%m%d%H%M%S")
);
match payload {
ExportPayload::Json(data) => {
let body = serde_json::to_string(&ApiResponse::ok(data))
@@ -849,22 +885,27 @@ where
.body(Body::from(body))
.unwrap())
}
ExportPayload::Csv(bytes) => {
Ok(axum::response::Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, "text/csv; charset=utf-8")
.header(header::CONTENT_DISPOSITION, format!("attachment; filename=\"{}.csv\"", filename))
.body(Body::from(bytes))
.unwrap())
}
ExportPayload::Xlsx(bytes) => {
Ok(axum::response::Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
.header(header::CONTENT_DISPOSITION, format!("attachment; filename=\"{}.xlsx\"", filename))
.body(Body::from(bytes))
.unwrap())
}
ExportPayload::Csv(bytes) => Ok(axum::response::Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, "text/csv; charset=utf-8")
.header(
header::CONTENT_DISPOSITION,
format!("attachment; filename=\"{}.csv\"", filename),
)
.body(Body::from(bytes))
.unwrap()),
ExportPayload::Xlsx(bytes) => Ok(axum::response::Response::builder()
.status(StatusCode::OK)
.header(
header::CONTENT_TYPE,
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
)
.header(
header::CONTENT_DISPOSITION,
format!("attachment; filename=\"{}.xlsx\"", filename),
)
.body(Body::from(bytes))
.unwrap()),
}
}
@@ -927,12 +968,8 @@ where
{
require_permission(&ctx, "plugin.admin")?;
let report = PluginDataService::reconcile_references(
plugin_id,
ctx.tenant_id,
&state.db,
)
.await?;
let report =
PluginDataService::reconcile_references(plugin_id, ctx.tenant_id, &state.db).await?;
Ok(Json(ApiResponse::ok(report)))
}
@@ -982,16 +1019,19 @@ where
let mid = manifest_id.clone();
let ent = entity.clone();
let items = rows.into_iter().map(|r| UserViewResp {
id: r.id.to_string(),
plugin_id: mid.clone(),
entity_name: ent.clone(),
view_name: r.view_name,
view_config: r.view_config,
is_default: r.is_default,
created_at: r.created_at,
updated_at: r.updated_at,
}).collect();
let items = rows
.into_iter()
.map(|r| UserViewResp {
id: r.id.to_string(),
plugin_id: mid.clone(),
entity_name: ent.clone(),
view_name: r.view_name,
view_config: r.view_config,
is_default: r.is_default,
created_at: r.created_at,
updated_at: r.updated_at,
})
.collect();
Ok(Json(ApiResponse::ok(items)))
}
@@ -1067,11 +1107,15 @@ where
PluginState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
state.db.execute(Statement::from_sql_and_values(
sea_orm::DatabaseBackend::Postgres,
"DELETE FROM plugin_user_views WHERE id = $1 AND tenant_id = $2 AND user_id = $3",
[view_id.into(), ctx.tenant_id.into(), ctx.user_id.into()],
)).await.map_err(|e| AppError::Internal(e.to_string()))?;
state
.db
.execute(Statement::from_sql_and_values(
sea_orm::DatabaseBackend::Postgres,
"DELETE FROM plugin_user_views WHERE id = $1 AND tenant_id = $2 AND user_id = $3",
[view_id.into(), ctx.tenant_id.into(), ctx.user_id.into()],
))
.await
.map_err(|e| AppError::Internal(e.to_string()))?;
Ok(Json(ApiResponse::ok(())))
}

View File

@@ -1,8 +1,11 @@
use axum::Extension;
use axum::extract::{FromRef, Path, Query, State};
use axum::response::Json;
use axum::Extension;
use chrono::Utc;
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder, Set, prelude::Decimal};
use sea_orm::{
ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder, Set,
prelude::Decimal,
};
use uuid::Uuid;
use erp_core::error::AppError;
@@ -64,8 +67,8 @@ where
let page = params.page.unwrap_or(1);
let page_size = params.page_size.unwrap_or(20).min(100);
let mut query = market_entry::Entity::find()
.filter(market_entry::Column::Status.eq("published"));
let mut query =
market_entry::Entity::find().filter(market_entry::Column::Status.eq("published"));
if let Some(ref category) = params.category {
query = query.filter(market_entry::Column::Category.eq(category.as_str()));
@@ -82,7 +85,11 @@ where
query = query.order_by_desc(market_entry::Column::DownloadCount);
let total = query.clone().count(db).await.map_err(|e| AppError::Internal(e.to_string()))?;
let total = query
.clone()
.count(db)
.await
.map_err(|e| AppError::Internal(e.to_string()))?;
let total_pages = ((total as f64) / (page_size as f64)).ceil() as u64;
let models = query
@@ -194,7 +201,9 @@ where
.map_err(|e| AppError::Internal(e.to_string()))?;
if existing.is_some() {
return Err(AppError::Validation("该插件已安装,如需更新请使用升级功能".to_string()));
return Err(AppError::Validation(
"该插件已安装,如需更新请使用升级功能".to_string(),
));
}
// upload → install → enable 一条龙
@@ -207,30 +216,26 @@ where
wasm_binary,
&manifest_toml,
db,
).await?;
)
.await?;
let plugin_id = plugin_resp.id;
let _plugin_resp = crate::service::PluginService::install(
plugin_id,
ctx.tenant_id,
ctx.user_id,
db,
engine,
).await?;
let _plugin_resp =
crate::service::PluginService::install(plugin_id, ctx.tenant_id, ctx.user_id, db, engine)
.await?;
let plugin_resp = crate::service::PluginService::enable(
plugin_id,
ctx.tenant_id,
ctx.user_id,
db,
engine,
).await?;
let plugin_resp =
crate::service::PluginService::enable(plugin_id, ctx.tenant_id, ctx.user_id, db, engine)
.await?;
// 递增下载计数
let mut active: market_entry::ActiveModel = market_model.into();
let current = active.download_count.take().unwrap_or(0);
active.download_count = Set(current + 1);
let _ = active.update(db).await.map_err(|e| AppError::Internal(e.to_string()))?;
let _ = active
.update(db)
.await
.map_err(|e| AppError::Internal(e.to_string()))?;
Ok(Json(ApiResponse::ok(plugin_resp)))
}
@@ -263,14 +268,17 @@ where
.await
.map_err(|e| AppError::Internal(e.to_string()))?;
let items = reviews.iter().map(|r| MarketReviewResp {
id: r.id.to_string(),
user_id: r.user_id.to_string(),
market_entry_id: r.market_entry_id.to_string(),
rating: r.rating,
review_text: r.review_text.clone(),
created_at: Some(r.created_at),
}).collect();
let items = reviews
.iter()
.map(|r| MarketReviewResp {
id: r.id.to_string(),
user_id: r.user_id.to_string(),
market_entry_id: r.market_entry_id.to_string(),
rating: r.rating,
review_text: r.review_text.clone(),
created_at: Some(r.created_at),
})
.collect();
Ok(Json(ApiResponse::ok(items)))
}
@@ -322,7 +330,10 @@ where
let mut active: market_review::ActiveModel = existing.into();
active.rating = Set(body.rating);
active.review_text = Set(body.review_text);
active.update(db).await.map_err(|e| AppError::Internal(e.to_string()))?
active
.update(db)
.await
.map_err(|e| AppError::Internal(e.to_string()))?
} else {
let review_id = Uuid::now_v7();
let now = Utc::now();
@@ -335,7 +346,10 @@ where
review_text: Set(body.review_text),
created_at: Set(now),
};
model.insert(db).await.map_err(|e| AppError::Internal(e.to_string()))?
model
.insert(db)
.await
.map_err(|e| AppError::Internal(e.to_string()))?
};
// 重新计算平均评分
@@ -356,7 +370,10 @@ where
let avg_decimal = Decimal::from_f64_retain(avg).unwrap_or_default();
entry_active.rating_avg = Set(avg_decimal);
entry_active.rating_count = Set(count);
let _ = entry_active.update(db).await.map_err(|e| AppError::Internal(e.to_string()))?;
let _ = entry_active
.update(db)
.await
.map_err(|e| AppError::Internal(e.to_string()))?;
Ok(Json(ApiResponse::ok(MarketReviewResp {
id: review_model.id.to_string(),

View File

@@ -7,9 +7,7 @@ use erp_core::error::AppError;
use erp_core::rbac::require_permission;
use erp_core::types::{ApiResponse, PaginatedResponse, Pagination, TenantContext};
use crate::dto::{
PluginHealthResp, PluginListParams, PluginResp, UpdatePluginConfigReq,
};
use crate::dto::{PluginHealthResp, PluginListParams, PluginResp, UpdatePluginConfigReq};
use crate::service::PluginService;
use crate::state::PluginState;
@@ -39,20 +37,27 @@ where
let mut wasm_binary: Option<Vec<u8>> = None;
let mut manifest_toml: Option<String> = None;
while let Some(field) = multipart.next_field().await.map_err(|e| {
AppError::Validation(format!("Multipart 解析失败: {}", e))
})? {
while let Some(field) = multipart
.next_field()
.await
.map_err(|e| AppError::Validation(format!("Multipart 解析失败: {}", e)))?
{
let name = field.name().unwrap_or("");
match name {
"wasm" => {
wasm_binary = Some(field.bytes().await.map_err(|e| {
AppError::Validation(format!("读取 WASM 文件失败: {}", e))
})?.to_vec());
wasm_binary = Some(
field
.bytes()
.await
.map_err(|e| AppError::Validation(format!("读取 WASM 文件失败: {}", e)))?
.to_vec(),
);
}
"manifest" => {
let bytes = field.bytes().await.map_err(|e| {
AppError::Validation(format!("读取 Manifest 失败: {}", e))
})?;
let bytes = field
.bytes()
.await
.map_err(|e| AppError::Validation(format!("读取 Manifest 失败: {}", e)))?;
let text = String::from_utf8(bytes.to_vec()).map_err(|e| {
AppError::Validation(format!("Manifest 不是有效的 UTF-8: {}", e))
})?;
@@ -62,21 +67,12 @@ where
}
}
let wasm = wasm_binary.ok_or_else(|| {
AppError::Validation("缺少 wasm 文件".to_string())
})?;
let manifest = manifest_toml.ok_or_else(|| {
AppError::Validation("缺少 manifest 文件".to_string())
})?;
let wasm = wasm_binary.ok_or_else(|| AppError::Validation("缺少 wasm 文件".to_string()))?;
let manifest =
manifest_toml.ok_or_else(|| AppError::Validation("缺少 manifest 文件".to_string()))?;
let result = PluginService::upload(
ctx.tenant_id,
ctx.user_id,
wasm,
&manifest,
&state.db,
)
.await?;
let result =
PluginService::upload(ctx.tenant_id, ctx.user_id, wasm, &manifest, &state.db).await?;
Ok(Json(ApiResponse::ok(result)))
}
@@ -195,18 +191,12 @@ where
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "plugin.admin")?;
let result = PluginService::install(
id,
ctx.tenant_id,
ctx.user_id,
&state.db,
&state.engine,
)
.await
.map_err(|e| {
tracing::error!(error = %e, "Install failed");
e
})?;
let result = PluginService::install(id, ctx.tenant_id, ctx.user_id, &state.db, &state.engine)
.await
.map_err(|e| {
tracing::error!(error = %e, "Install failed");
e
})?;
Ok(Json(ApiResponse::ok(result)))
}
@@ -230,14 +220,8 @@ where
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "plugin.admin")?;
let result = PluginService::enable(
id,
ctx.tenant_id,
ctx.user_id,
&state.db,
&state.engine,
)
.await?;
let result =
PluginService::enable(id, ctx.tenant_id, ctx.user_id, &state.db, &state.engine).await?;
Ok(Json(ApiResponse::ok(result)))
}
@@ -261,14 +245,8 @@ where
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "plugin.admin")?;
let result = PluginService::disable(
id,
ctx.tenant_id,
ctx.user_id,
&state.db,
&state.engine,
)
.await?;
let result =
PluginService::disable(id, ctx.tenant_id, ctx.user_id, &state.db, &state.engine).await?;
Ok(Json(ApiResponse::ok(result)))
}
@@ -292,14 +270,8 @@ where
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "plugin.admin")?;
let result = PluginService::uninstall(
id,
ctx.tenant_id,
ctx.user_id,
&state.db,
&state.engine,
)
.await?;
let result =
PluginService::uninstall(id, ctx.tenant_id, ctx.user_id, &state.db, &state.engine).await?;
Ok(Json(ApiResponse::ok(result)))
}
@@ -373,8 +345,12 @@ where
require_permission(&ctx, "plugin.list")?;
// 通过 plugin_id 找到 manifest_id再查询 metrics
let manifest_id = crate::data_service::resolve_manifest_id(id, ctx.tenant_id, &state.db).await?;
let metrics = state.engine.get_metrics(&manifest_id).await
let manifest_id =
crate::data_service::resolve_manifest_id(id, ctx.tenant_id, &state.db).await?;
let metrics = state
.engine
.get_metrics(&manifest_id)
.await
.map_err(|e| AppError::Internal(e.to_string()))?;
let avg_ms = if metrics.total_invocations > 0 {
@@ -457,20 +433,27 @@ where
let mut wasm_binary: Option<Vec<u8>> = None;
let mut manifest_toml: Option<String> = None;
while let Some(field) = multipart.next_field().await.map_err(|e| {
AppError::Validation(format!("Multipart 解析失败: {}", e))
})? {
while let Some(field) = multipart
.next_field()
.await
.map_err(|e| AppError::Validation(format!("Multipart 解析失败: {}", e)))?
{
let name = field.name().unwrap_or("");
match name {
"wasm" => {
wasm_binary = Some(field.bytes().await.map_err(|e| {
AppError::Validation(format!("读取 WASM 文件失败: {}", e))
})?.to_vec());
wasm_binary = Some(
field
.bytes()
.await
.map_err(|e| AppError::Validation(format!("读取 WASM 文件失败: {}", e)))?
.to_vec(),
);
}
"manifest" => {
let bytes = field.bytes().await.map_err(|e| {
AppError::Validation(format!("读取 Manifest 失败: {}", e))
})?;
let bytes = field
.bytes()
.await
.map_err(|e| AppError::Validation(format!("读取 Manifest 失败: {}", e)))?;
manifest_toml = Some(String::from_utf8(bytes.to_vec()).map_err(|e| {
AppError::Validation(format!("Manifest 不是有效的 UTF-8: {}", e))
})?);
@@ -479,12 +462,9 @@ where
}
}
let wasm = wasm_binary.ok_or_else(|| {
AppError::Validation("缺少 wasm 文件".to_string())
})?;
let manifest = manifest_toml.ok_or_else(|| {
AppError::Validation("缺少 manifest 文件".to_string())
})?;
let wasm = wasm_binary.ok_or_else(|| AppError::Validation("缺少 wasm 文件".to_string()))?;
let manifest =
manifest_toml.ok_or_else(|| AppError::Validation("缺少 manifest 文件".to_string()))?;
let result = PluginService::upgrade(
id,
@@ -525,6 +505,7 @@ where
serde_json::from_value(model.manifest_json.clone())
.map_err(|e| AppError::Validation(format!("manifest 解析失败: {}", e)))?;
let report = crate::plugin_validator::validate_plugin_security(&manifest, model.wasm_binary.len())?;
let report =
crate::plugin_validator::validate_plugin_security(&manifest, model.wasm_binary.len())?;
Ok(Json(ApiResponse::ok(report)))
}