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(())))
}