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:
@@ -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(¶ms.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(())
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user