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

@@ -39,22 +39,20 @@ struct ApiDoc;
erp_auth::handler::role_handler::get_role_permissions,
erp_auth::handler::role_handler::list_permissions,
),
components(
schemas(
erp_auth::dto::LoginReq,
erp_auth::dto::LoginResp,
erp_auth::dto::RefreshReq,
erp_auth::dto::UserResp,
erp_auth::dto::CreateUserReq,
erp_auth::dto::UpdateUserReq,
erp_auth::dto::RoleResp,
erp_auth::dto::CreateRoleReq,
erp_auth::dto::UpdateRoleReq,
erp_auth::dto::PermissionResp,
erp_auth::dto::AssignPermissionsReq,
erp_auth::dto::ChangePasswordReq,
)
)
components(schemas(
erp_auth::dto::LoginReq,
erp_auth::dto::LoginResp,
erp_auth::dto::RefreshReq,
erp_auth::dto::UserResp,
erp_auth::dto::CreateUserReq,
erp_auth::dto::UpdateUserReq,
erp_auth::dto::RoleResp,
erp_auth::dto::CreateRoleReq,
erp_auth::dto::UpdateRoleReq,
erp_auth::dto::PermissionResp,
erp_auth::dto::AssignPermissionsReq,
erp_auth::dto::ChangePasswordReq,
))
)]
struct AuthApiDoc;
@@ -86,23 +84,21 @@ struct AuthApiDoc;
erp_config::handler::setting_handler::update_setting,
erp_config::handler::setting_handler::delete_setting,
),
components(
schemas(
erp_config::dto::DictionaryResp,
erp_config::dto::CreateDictionaryReq,
erp_config::dto::UpdateDictionaryReq,
erp_config::dto::DictionaryItemResp,
erp_config::dto::CreateDictionaryItemReq,
erp_config::dto::UpdateDictionaryItemReq,
erp_config::dto::MenuResp,
erp_config::dto::CreateMenuReq,
erp_config::dto::UpdateMenuReq,
erp_config::dto::NumberingRuleResp,
erp_config::dto::CreateNumberingRuleReq,
erp_config::dto::UpdateNumberingRuleReq,
erp_config::dto::ThemeResp,
)
)
components(schemas(
erp_config::dto::DictionaryResp,
erp_config::dto::CreateDictionaryReq,
erp_config::dto::UpdateDictionaryReq,
erp_config::dto::DictionaryItemResp,
erp_config::dto::CreateDictionaryItemReq,
erp_config::dto::UpdateDictionaryItemReq,
erp_config::dto::MenuResp,
erp_config::dto::CreateMenuReq,
erp_config::dto::UpdateMenuReq,
erp_config::dto::NumberingRuleResp,
erp_config::dto::CreateNumberingRuleReq,
erp_config::dto::UpdateNumberingRuleReq,
erp_config::dto::ThemeResp,
))
)]
struct ConfigApiDoc;
@@ -126,18 +122,16 @@ struct ConfigApiDoc;
erp_workflow::handler::task_handler::complete_task,
erp_workflow::handler::task_handler::delegate_task,
),
components(
schemas(
erp_workflow::dto::ProcessDefinitionResp,
erp_workflow::dto::CreateProcessDefinitionReq,
erp_workflow::dto::UpdateProcessDefinitionReq,
erp_workflow::dto::ProcessInstanceResp,
erp_workflow::dto::StartInstanceReq,
erp_workflow::dto::TaskResp,
erp_workflow::dto::CompleteTaskReq,
erp_workflow::dto::DelegateTaskReq,
)
)
components(schemas(
erp_workflow::dto::ProcessDefinitionResp,
erp_workflow::dto::CreateProcessDefinitionReq,
erp_workflow::dto::UpdateProcessDefinitionReq,
erp_workflow::dto::ProcessInstanceResp,
erp_workflow::dto::StartInstanceReq,
erp_workflow::dto::TaskResp,
erp_workflow::dto::CompleteTaskReq,
erp_workflow::dto::DelegateTaskReq,
))
)]
struct WorkflowApiDoc;
@@ -155,18 +149,16 @@ struct WorkflowApiDoc;
erp_message::handler::template_handler::create_template,
erp_message::handler::subscription_handler::update_subscription,
),
components(
schemas(
erp_message::dto::MessageResp,
erp_message::dto::SendMessageReq,
erp_message::dto::MessageQuery,
erp_message::dto::UnreadCountResp,
erp_message::dto::MessageTemplateResp,
erp_message::dto::CreateTemplateReq,
erp_message::dto::MessageSubscriptionResp,
erp_message::dto::UpdateSubscriptionReq,
)
)
components(schemas(
erp_message::dto::MessageResp,
erp_message::dto::SendMessageReq,
erp_message::dto::MessageQuery,
erp_message::dto::UnreadCountResp,
erp_message::dto::MessageTemplateResp,
erp_message::dto::CreateTemplateReq,
erp_message::dto::MessageSubscriptionResp,
erp_message::dto::UpdateSubscriptionReq,
))
)]
struct MessageApiDoc;
@@ -190,31 +182,31 @@ async fn main() -> anyhow::Result<()> {
let config = AppConfig::load()?;
// ── 安全检查:拒绝默认密钥 ──────────────────────────
if config.jwt.secret == "__MUST_SET_VIA_ENV__" || config.jwt.secret == "change-me-in-production" {
tracing::error!(
"JWT 密钥为默认值,拒绝启动。请设置环境变量 ERP__JWT__SECRET"
);
if config.jwt.secret == "__MUST_SET_VIA_ENV__" || config.jwt.secret == "change-me-in-production"
{
tracing::error!("JWT 密钥为默认值,拒绝启动。请设置环境变量 ERP__JWT__SECRET");
std::process::exit(1);
}
if config.database.url == "__MUST_SET_VIA_ENV__" {
tracing::error!(
"数据库 URL 为默认占位值,拒绝启动。请设置环境变量 ERP__DATABASE__URL"
);
tracing::error!("数据库 URL 为默认占位值,拒绝启动。请设置环境变量 ERP__DATABASE__URL");
std::process::exit(1);
}
if config.redis.url == "__MUST_SET_VIA_ENV__" {
tracing::error!(
"Redis URL 为默认占位值,拒绝启动。请设置环境变量 ERP__REDIS__URL"
);
tracing::error!("Redis URL 为默认占位值,拒绝启动。请设置环境变量 ERP__REDIS__URL");
std::process::exit(1);
}
if !config.wechat.dev_mode && (config.wechat.appid == "__MUST_SET_VIA_ENV__" || config.wechat.secret == "__MUST_SET_VIA_ENV__") {
if !config.wechat.dev_mode
&& (config.wechat.appid == "__MUST_SET_VIA_ENV__"
|| config.wechat.secret == "__MUST_SET_VIA_ENV__")
{
tracing::error!(
"微信凭据为默认占位值,拒绝启动。请设置环境变量 ERP__WECHAT__APPID 和 ERP__WECHAT__SECRET"
);
std::process::exit(1);
}
if config.health.aes_key == "__MUST_SET_VIA_ENV__" || config.health.hmac_key == "__MUST_SET_VIA_ENV__" {
if config.health.aes_key == "__MUST_SET_VIA_ENV__"
|| config.health.hmac_key == "__MUST_SET_VIA_ENV__"
{
// 注: health 密钥已被统一 KEK (ERP__CRYPTO__KEK) 替代,此处仅保留兼容性检查
tracing::warn!(
"ERP__HEALTH__AES_KEY/HMAC_KEY 未设置(已迁移到 ERP__CRYPTO__KEK 统一密钥体系)"
@@ -292,12 +284,21 @@ async fn main() -> anyhow::Result<()> {
tracing::info!(tenant_id = %new_tenant_id, "Default tenant ready with auth seed data");
// Seed AI workflow definitions
if let Err(e) = erp_workflow::service::ai_workflow_seed::ensure_ai_workflows(&db, new_tenant_id).await {
if let Err(e) =
erp_workflow::service::ai_workflow_seed::ensure_ai_workflows(&db, new_tenant_id)
.await
{
tracing::warn!(error = %e, "Failed to seed AI workflow definitions");
}
// Seed dialysis session workflow definition
if let Err(e) = dialysis_workflow::seed_dialysis_session_workflow(&db, new_tenant_id, new_tenant_id).await {
if let Err(e) = dialysis_workflow::seed_dialysis_session_workflow(
&db,
new_tenant_id,
new_tenant_id,
)
.await
{
tracing::warn!(error = %e, "Failed to seed dialysis session workflow");
}
@@ -363,7 +364,6 @@ async fn main() -> anyhow::Result<()> {
// Points module 已统一到 erp-health/health/points/* 路由)
// Initialize dialysis module
let dialysis_module = erp_dialysis::DialysisModule;
tracing::info!(
@@ -388,11 +388,8 @@ async fn main() -> anyhow::Result<()> {
// Initialize plugin engine
let plugin_config = erp_plugin::engine::PluginEngineConfig::default();
let plugin_engine = erp_plugin::engine::PluginEngine::new(
db.clone(),
event_bus.clone(),
plugin_config,
)?;
let plugin_engine =
erp_plugin::engine::PluginEngine::new(db.clone(), event_bus.clone(), plugin_config)?;
tracing::info!("Plugin engine initialized");
// Register plugin module
@@ -466,7 +463,9 @@ async fn main() -> anyhow::Result<()> {
}
#[cfg(not(debug_assertions))]
{
panic!("ERP__CRYPTO__KEK must be set in production. Use a 64-char hex string (32 bytes).");
panic!(
"ERP__CRYPTO__KEK must be set in production. Use a 64-char hex string (32 bytes)."
);
}
} else {
erp_core::crypto::PiiCrypto::from_kek_hex(&config.crypto.kek)
@@ -480,9 +479,8 @@ async fn main() -> anyhow::Result<()> {
// 始终注册默认 Claude provider兼容旧配置
{
let mut claude = erp_ai::provider::claude::ClaudeProvider::new(
config.ai.api_key.clone(),
);
let mut claude =
erp_ai::provider::claude::ClaudeProvider::new(config.ai.api_key.clone());
if let Some(ref base_url) = config.ai.base_url {
claude = claude.with_base_url(base_url.clone());
}
@@ -496,22 +494,31 @@ async fn main() -> anyhow::Result<()> {
}
match pcfg.provider_type.as_str() {
"openai" => {
let api_key = pcfg.api_key_env.as_ref()
let api_key = pcfg
.api_key_env
.as_ref()
.and_then(|env| std::env::var(env).ok())
.unwrap_or_default();
let base_url = pcfg.base_url.clone()
let base_url = pcfg
.base_url
.clone()
.unwrap_or_else(|| "https://api.openai.com".to_string());
let provider = erp_ai::provider::openai::OpenAIProvider::new(
api_key, base_url, pcfg.default_model.clone(),
api_key,
base_url,
pcfg.default_model.clone(),
);
registry.register(name.clone(), std::sync::Arc::new(provider));
tracing::info!(provider = %name, "已注册 OpenAI 兼容提供商");
}
"ollama" => {
let base_url = pcfg.base_url.clone()
let base_url = pcfg
.base_url
.clone()
.unwrap_or_else(|| "http://localhost:11434".to_string());
let provider = erp_ai::provider::ollama::OllamaProvider::new(
base_url, pcfg.default_model.clone(),
base_url,
pcfg.default_model.clone(),
);
registry.register(name.clone(), std::sync::Arc::new(provider));
tracing::info!(provider = %name, "已注册 Ollama 本地提供商");
@@ -528,46 +535,58 @@ async fn main() -> anyhow::Result<()> {
tracing::info!(providers = ?registry.provider_names(), "AI Provider 注册完成");
// 根据 default_provider 配置构建 AnalysisService 的默认 provider
let default_provider: Box<dyn erp_ai::provider::AiProvider> =
match config.ai.default_provider.as_str() {
"ollama" => {
let pcfg = config.ai.providers.get("ollama");
let base_url = pcfg.and_then(|c| c.base_url.clone())
.unwrap_or_else(|| "http://localhost:11434".to_string());
let model = pcfg.map(|c| c.default_model.clone())
.unwrap_or_else(|| config.ai.model.clone());
tracing::info!(base_url = %base_url, model = %model, "AnalysisService 使用 Ollama 提供商");
Box::new(erp_ai::provider::ollama::OllamaProvider::new(base_url, model))
let default_provider: Box<dyn erp_ai::provider::AiProvider> = match config
.ai
.default_provider
.as_str()
{
"ollama" => {
let pcfg = config.ai.providers.get("ollama");
let base_url = pcfg
.and_then(|c| c.base_url.clone())
.unwrap_or_else(|| "http://localhost:11434".to_string());
let model = pcfg
.map(|c| c.default_model.clone())
.unwrap_or_else(|| config.ai.model.clone());
tracing::info!(base_url = %base_url, model = %model, "AnalysisService 使用 Ollama 提供商");
Box::new(erp_ai::provider::ollama::OllamaProvider::new(
base_url, model,
))
}
"openai" => {
let pcfg = config.ai.providers.get("openai");
let api_key = pcfg
.and_then(|c| c.api_key_env.as_ref())
.and_then(|env| std::env::var(env).ok())
.unwrap_or_default();
let base_url = pcfg
.and_then(|c| c.base_url.clone())
.unwrap_or_else(|| "https://api.openai.com".to_string());
let model = pcfg
.map(|c| c.default_model.clone())
.unwrap_or_else(|| config.ai.model.clone());
Box::new(erp_ai::provider::openai::OpenAIProvider::new(
api_key, base_url, model,
))
}
_ => {
// 默认 Claude
let mut claude =
erp_ai::provider::claude::ClaudeProvider::new(config.ai.api_key.clone());
if let Some(ref base_url) = config.ai.base_url {
claude = claude.with_base_url(base_url.clone());
}
"openai" => {
let pcfg = config.ai.providers.get("openai");
let api_key = pcfg.and_then(|c| c.api_key_env.as_ref())
.and_then(|env| std::env::var(env).ok())
.unwrap_or_default();
let base_url = pcfg.and_then(|c| c.base_url.clone())
.unwrap_or_else(|| "https://api.openai.com".to_string());
let model = pcfg.map(|c| c.default_model.clone())
.unwrap_or_else(|| config.ai.model.clone());
Box::new(erp_ai::provider::openai::OpenAIProvider::new(api_key, base_url, model))
}
_ => {
// 默认 Claude
let mut claude = erp_ai::provider::claude::ClaudeProvider::new(
config.ai.api_key.clone(),
);
if let Some(ref base_url) = config.ai.base_url {
claude = claude.with_base_url(base_url.clone());
}
Box::new(claude)
}
};
Box::new(claude)
}
};
let analysis_svc = erp_ai::service::analysis::AnalysisService::new(
default_provider,
db.clone(),
).with_knowledge_source(std::sync::Arc::new(
erp_ai::knowledge::structured_source::StructuredKnowledgeSource::new(db.clone()),
));
let analysis_svc =
erp_ai::service::analysis::AnalysisService::new(default_provider, db.clone())
.with_knowledge_source(std::sync::Arc::new(
erp_ai::knowledge::structured_source::StructuredKnowledgeSource::new(
db.clone(),
),
));
let analysis = std::sync::Arc::new(analysis_svc);
let prompt = std::sync::Arc::new(erp_ai::service::prompt::PromptService::new(db.clone()));
let usage = std::sync::Arc::new(erp_ai::service::usage::UsageService::new(db.clone()));
@@ -684,6 +703,9 @@ async fn main() -> anyhow::Result<()> {
"/analytics/batch",
axum::routing::post(handlers::analytics::batch),
)
.layer(axum::middleware::from_fn(
middleware::frozen_module::frozen_module_middleware,
))
.layer(axum::middleware::from_fn_with_state(
state.clone(),
middleware::rate_limit::rate_limit_by_user,
@@ -716,9 +738,15 @@ async fn main() -> anyhow::Result<()> {
let secret = secret_for_uploads.clone();
async move { upload_auth_middleware(secret, req, next).await }
}));
let fhir_routes = erp_health::HealthModule::fhir_routes().with_state(state.clone());
let app = Router::new()
.nest("/api/v1", unthrottled_routes.merge(public_routes).merge(protected_routes))
.nest("/fhir", erp_health::HealthModule::fhir_routes().with_state(state.clone()))
.nest(
"/api/v1",
unthrottled_routes
.merge(public_routes)
.merge(protected_routes)
.nest("/fhir", fhir_routes),
)
.nest(
"/health/gateway",
erp_health::HealthModule::gateway_routes()
@@ -729,7 +757,9 @@ async fn main() -> anyhow::Result<()> {
.with_state(state.clone()),
)
.nest("/uploads", uploads_router)
.layer(axum::middleware::from_fn(middleware::metrics::metrics_middleware))
.layer(axum::middleware::from_fn(
middleware::metrics::metrics_middleware,
))
.layer(cors);
// Start Prometheus metrics exporter on a separate port
@@ -811,7 +841,9 @@ fn build_cors_layer(allowed_origins: &str) -> tower_http::cors::CorsLayer {
#[cfg(not(debug_assertions))]
{
tracing::error!("CORS wildcard '*' is not allowed in production builds");
panic!("Refusing to start with CORS wildcard in release mode. Set ERP__CORS__ALLOWED_ORIGINS to specific domains.");
panic!(
"Refusing to start with CORS wildcard in release mode. Set ERP__CORS__ALLOWED_ORIGINS to specific domains."
);
}
#[cfg(debug_assertions)]
{
@@ -879,6 +911,7 @@ async fn shutdown_signal() {
/// 对每个模块的 `permissions()` 返回的权限执行 upsert
/// - 新权限INSERT
/// - 已有权限(同 tenant_id + code跳过
///
/// 同时将新权限分配给 admin 角色。
async fn sync_module_permissions(
db: &sea_orm::DatabaseConnection,
@@ -906,7 +939,7 @@ async fn sync_module_permissions(
perm.code.clone().into(),
perm.name.clone().into(),
perm.module.clone().into(),
perm.code.split('.').last().unwrap_or("manage").into(),
perm.code.split('.').next_back().unwrap_or("manage").into(),
perm.description.clone().into(),
system_user_id.into(),
],
@@ -932,7 +965,10 @@ async fn sync_module_permissions(
)).await?;
if total_new > 0 {
tracing::info!(total_new, "New module permissions synced and bound to admin role");
tracing::info!(
total_new,
"New module permissions synced and bound to admin role"
);
}
Ok(())