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

@@ -1,6 +1,6 @@
use axum::Json;
use axum::extract::{Extension, FromRef, Path, Query, State};
use axum::response::sse::{Event, KeepAlive, Sse};
use axum::Json;
use erp_core::health_provider::TimeRange;
use erp_core::rbac::require_permission;
use erp_core::types::{ApiResponse, TenantContext};
@@ -34,9 +34,9 @@ where
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "ai.analysis.manage")?;
let report_id = body.report_id.ok_or_else(|| {
erp_core::error::AppError::Validation("report_id 必填".into())
})?;
let report_id = body
.report_id
.ok_or_else(|| erp_core::error::AppError::Validation("report_id 必填".into()))?;
let lab_dto = state
.health_provider
@@ -57,7 +57,10 @@ where
.await?;
let model_config = &prompt.model_config;
let model = model_config["model"].as_str().unwrap_or("claude-sonnet-4-6").to_string();
let model = model_config["model"]
.as_str()
.unwrap_or("claude-sonnet-4-6")
.to_string();
let temperature = model_config["temperature"].as_f64().unwrap_or(0.3) as f32;
let max_tokens = model_config["max_tokens"].as_u64().unwrap_or(2048) as u32;
@@ -83,7 +86,15 @@ where
let patient_id_clone = uuid::Uuid::nil(); // lab report 场景 patient_id 从 report 关联
let doctor_id_clone = ctx.user_id;
let sse_stream = build_sse_stream(stream, analysis_id_clone, state_clone, "lab_report", ctx.tenant_id, patient_id_clone, doctor_id_clone);
let sse_stream = build_sse_stream(
stream,
analysis_id_clone,
state_clone,
"lab_report",
ctx.tenant_id,
patient_id_clone,
doctor_id_clone,
);
Ok(Sse::new(sse_stream).keep_alive(KeepAlive::default()))
}
@@ -97,9 +108,9 @@ where
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "ai.analysis.manage")?;
let patient_id = body.patient_id.ok_or_else(|| {
erp_core::error::AppError::Validation("patient_id 必填".into())
})?;
let patient_id = body
.patient_id
.ok_or_else(|| erp_core::error::AppError::Validation("patient_id 必填".into()))?;
let metrics = body.metrics.unwrap_or_else(|| {
vec![
@@ -126,7 +137,10 @@ where
));
}
let sanitized_data = state.analysis.sanitizer.sanitize_trend_analysis(&trend_data)?;
let sanitized_data = state
.analysis
.sanitizer
.sanitize_trend_analysis(&trend_data)?;
let prompt = state
.prompt
@@ -134,7 +148,10 @@ where
.await?;
let model_config = &prompt.model_config;
let model = model_config["model"].as_str().unwrap_or("claude-sonnet-4-6").to_string();
let model = model_config["model"]
.as_str()
.unwrap_or("claude-sonnet-4-6")
.to_string();
let temperature = model_config["temperature"].as_f64().unwrap_or(0.3) as f32;
let max_tokens = model_config["max_tokens"].as_u64().unwrap_or(2048) as u32;
@@ -158,7 +175,15 @@ where
let analysis_id_clone = analysis_id;
let state_clone = state.clone();
let sse_stream = build_sse_stream(stream, analysis_id_clone, state_clone, "trend", ctx.tenant_id, uuid::Uuid::nil(), ctx.user_id);
let sse_stream = build_sse_stream(
stream,
analysis_id_clone,
state_clone,
"trend",
ctx.tenant_id,
uuid::Uuid::nil(),
ctx.user_id,
);
Ok(Sse::new(sse_stream).keep_alive(KeepAlive::default()))
}
@@ -172,9 +197,9 @@ where
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "ai.analysis.manage")?;
let patient_id = body.patient_id.ok_or_else(|| {
erp_core::error::AppError::Validation("patient_id 必填".into())
})?;
let patient_id = body
.patient_id
.ok_or_else(|| erp_core::error::AppError::Validation("patient_id 必填".into()))?;
let summary_dto = state
.health_provider
@@ -191,7 +216,10 @@ where
.await?;
let model_config = &prompt.model_config;
let model = model_config["model"].as_str().unwrap_or("claude-sonnet-4-6").to_string();
let model = model_config["model"]
.as_str()
.unwrap_or("claude-sonnet-4-6")
.to_string();
let temperature = model_config["temperature"].as_f64().unwrap_or(0.3) as f32;
let max_tokens = model_config["max_tokens"].as_u64().unwrap_or(2048) as u32;
@@ -215,7 +243,15 @@ where
let analysis_id_clone = analysis_id;
let state_clone = state.clone();
let sse_stream = build_sse_stream(stream, analysis_id_clone, state_clone, "checkup_plan", ctx.tenant_id, uuid::Uuid::nil(), ctx.user_id);
let sse_stream = build_sse_stream(
stream,
analysis_id_clone,
state_clone,
"checkup_plan",
ctx.tenant_id,
uuid::Uuid::nil(),
ctx.user_id,
);
Ok(Sse::new(sse_stream).keep_alive(KeepAlive::default()))
}
@@ -229,9 +265,9 @@ where
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "ai.analysis.manage")?;
let report_id = body.report_id.ok_or_else(|| {
erp_core::error::AppError::Validation("report_id 必填".into())
})?;
let report_id = body
.report_id
.ok_or_else(|| erp_core::error::AppError::Validation("report_id 必填".into()))?;
let report_dto = state
.health_provider
@@ -255,7 +291,10 @@ where
.await?;
let model_config = &prompt.model_config;
let model = model_config["model"].as_str().unwrap_or("claude-sonnet-4-6").to_string();
let model = model_config["model"]
.as_str()
.unwrap_or("claude-sonnet-4-6")
.to_string();
let temperature = model_config["temperature"].as_f64().unwrap_or(0.3) as f32;
let max_tokens = model_config["max_tokens"].as_u64().unwrap_or(2048) as u32;
@@ -279,7 +318,15 @@ where
let analysis_id_clone = analysis_id;
let state_clone = state.clone();
let sse_stream = build_sse_stream(stream, analysis_id_clone, state_clone, "report_summary", ctx.tenant_id, uuid::Uuid::nil(), ctx.user_id);
let sse_stream = build_sse_stream(
stream,
analysis_id_clone,
state_clone,
"report_summary",
ctx.tenant_id,
uuid::Uuid::nil(),
ctx.user_id,
);
Ok(Sse::new(sse_stream).keep_alive(KeepAlive::default()))
}
@@ -309,7 +356,12 @@ where
};
let (items, total) = state
.analysis
.list_analysis(ctx.tenant_id, params.patient_id, params.analysis_type, &pagination)
.list_analysis(
ctx.tenant_id,
params.patient_id,
params.analysis_type,
&pagination,
)
.await?;
// 批量查询 patient_name通过 raw SQL 避免跨 crate 依赖 erp-health
@@ -321,7 +373,10 @@ where
let patient_names: std::collections::HashMap<uuid::Uuid, String> = if !patient_ids.is_empty() {
#[derive(sea_orm::FromQueryResult)]
struct PatientName { id: uuid::Uuid, name: String }
struct PatientName {
id: uuid::Uuid,
name: String,
}
let ids: Vec<uuid::Uuid> = patient_ids.into_iter().collect();
use sea_orm::FromQueryResult;
PatientName::find_by_statement(sea_orm::Statement::from_sql_and_values(
@@ -339,15 +394,19 @@ where
std::collections::HashMap::new()
};
let data: Vec<serde_json::Value> = items.into_iter().map(|a| {
let mut val = serde_json::to_value(&a).unwrap_or_default();
if let Some(obj) = val.as_object_mut() {
obj.insert("patient_name".to_string(), serde_json::json!(
patient_names.get(&a.patient_id).cloned()
));
}
val
}).collect();
let data: Vec<serde_json::Value> = items
.into_iter()
.map(|a| {
let mut val = serde_json::to_value(&a).unwrap_or_default();
if let Some(obj) = val.as_object_mut() {
obj.insert(
"patient_name".to_string(),
serde_json::json!(patient_names.get(&a.patient_id).cloned()),
);
}
val
})
.collect();
Ok(Json(ApiResponse::ok(serde_json::json!({
"data": data,
@@ -549,7 +608,9 @@ where
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "ai.analysis.list")?;
Ok(Json(ApiResponse::ok(state.provider_registry.provider_names())))
Ok(Json(ApiResponse::ok(
state.provider_registry.provider_names(),
)))
}
pub async fn quota_summary<S>(
@@ -570,7 +631,10 @@ where
pub async fn assess_dialysis_risk<S>(
Extension(ctx): Extension<TenantContext>,
Json(body): Json<crate::service::dialysis_risk_scorer::DialysisLabInput>,
) -> Result<Json<ApiResponse<crate::service::dialysis_risk_scorer::DialysisRiskAssessment>>, erp_core::error::AppError>
) -> Result<
Json<ApiResponse<crate::service::dialysis_risk_scorer::DialysisRiskAssessment>>,
erp_core::error::AppError,
>
where
AiState: FromRef<S>,
S: Clone + Send + Sync + 'static,
@@ -612,7 +676,9 @@ where
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "ai.usage.list")?;
let model = params.model.unwrap_or_else(|| "claude-sonnet-4-6".to_string());
let model = params
.model
.unwrap_or_else(|| "claude-sonnet-4-6".to_string());
let estimate = crate::service::cost::CostService::estimate_cost(&params.analysis_type, &model);
Ok(Json(ApiResponse::ok(estimate)))
}
@@ -699,9 +765,10 @@ fn validate_prompt_safety(content: &str) -> Result<(), erp_core::error::AppError
let lower = content.to_lowercase();
for pattern in &suspicious {
if lower.contains(pattern) {
return Err(erp_core::error::AppError::Validation(
format!("提示词内容包含不安全模式: {}", pattern),
));
return Err(erp_core::error::AppError::Validation(format!(
"提示词内容包含不安全模式: {}",
pattern
)));
}
}
Ok(())

View File

@@ -1,5 +1,5 @@
use axum::extract::{Extension, FromRef, Path, State};
use axum::Json;
use axum::extract::{Extension, FromRef, Path, State};
use erp_core::rbac::require_permission;
use erp_core::types::{ApiResponse, TenantContext};
use serde::Deserialize;
@@ -26,19 +26,14 @@ where
require_permission(&ctx, "ai.suggestion.list")?;
if let Some(analysis_id) = params.analysis_id {
let items = SuggestionService::list_by_analysis(
&state.db,
ctx.tenant_id,
analysis_id,
)
.await?;
let items =
SuggestionService::list_by_analysis(&state.db, ctx.tenant_id, analysis_id).await?;
Ok(Json(ApiResponse::ok(serde_json::json!({
"data": items,
"total": items.len(),
}))))
} else {
let items =
SuggestionService::list_pending(&state.db, ctx.tenant_id).await?;
let items = SuggestionService::list_pending(&state.db, ctx.tenant_id).await?;
Ok(Json(ApiResponse::ok(serde_json::json!({
"data": items,
"total": items.len(),
@@ -69,7 +64,7 @@ where
_ => {
return Err(erp_core::error::AppError::Validation(
"action 必须为 approve 或 reject".into(),
))
));
}
};
@@ -151,7 +146,10 @@ where
match &suggestion.baseline_snapshot {
Some(bs) if !bs.is_null() => {
let action_result = suggestion.action_result.as_ref().unwrap_or(&serde_json::Value::Null);
let action_result = suggestion
.action_result
.as_ref()
.unwrap_or(&serde_json::Value::Null);
Ok(Json(ApiResponse::ok(serde_json::json!({
"suggestion_id": id,
"baseline": bs,