功能修复: 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 统一格式化
232 lines
7.2 KiB
Rust
232 lines
7.2 KiB
Rust
use erp_core::error::AppError;
|
||
|
||
#[derive(Debug, thiserror::Error)]
|
||
pub enum AiError {
|
||
#[error("验证失败: {0}")]
|
||
Validation(String),
|
||
|
||
#[error("分析未找到: {0}")]
|
||
AnalysisNotFound(String),
|
||
|
||
#[error("Prompt 模板未找到: {0}")]
|
||
PromptNotFound(String),
|
||
|
||
#[error("AI 提供商不可用: {0}")]
|
||
ProviderUnavailable(String),
|
||
|
||
#[error("AI 提供商错误: {0}")]
|
||
ProviderError(String),
|
||
|
||
#[error("数据脱敏失败: {0}")]
|
||
SanitizationError(String),
|
||
|
||
#[error("模板渲染失败: {0}")]
|
||
TemplateError(String),
|
||
|
||
#[error("速率超限")]
|
||
RateLimitExceeded,
|
||
|
||
#[error("版本不匹配")]
|
||
VersionMismatch,
|
||
|
||
#[error("数据库错误: {0}")]
|
||
DbError(String),
|
||
|
||
#[error("AI 配额已耗尽: {reason}")]
|
||
QuotaExhausted {
|
||
tenant_id: uuid::Uuid,
|
||
reason: String,
|
||
},
|
||
|
||
#[error("缓存错误: {0}")]
|
||
CacheError(String),
|
||
|
||
#[error("知识库错误: {0}")]
|
||
KnowledgeError(String),
|
||
|
||
#[error("分析队列错误: {0}")]
|
||
QueueError(String),
|
||
|
||
#[error("AI 配置错误: {0}")]
|
||
ConfigError(String),
|
||
}
|
||
|
||
impl From<AiError> for AppError {
|
||
fn from(e: AiError) -> Self {
|
||
match e {
|
||
AiError::Validation(msg) => AppError::Validation(msg),
|
||
AiError::AnalysisNotFound(id) => AppError::NotFound(format!("分析结果: {id}")),
|
||
AiError::PromptNotFound(name) => AppError::NotFound(format!("Prompt 模板: {name}")),
|
||
AiError::ProviderUnavailable(p) => AppError::Internal(format!("AI 提供商 {p} 不可用")),
|
||
AiError::RateLimitExceeded => AppError::TooManyRequests,
|
||
AiError::QuotaExhausted { .. } => AppError::TooManyRequests,
|
||
AiError::VersionMismatch => AppError::VersionMismatch,
|
||
AiError::DbError(msg) => AppError::Internal(msg),
|
||
other => AppError::Internal(other.to_string()),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl From<sea_orm::DbErr> for AiError {
|
||
fn from(e: sea_orm::DbErr) -> Self {
|
||
AiError::DbError(e.to_string())
|
||
}
|
||
}
|
||
|
||
pub type AiResult<T> = Result<T, AiError>;
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn validation_maps_to_app_error_validation() {
|
||
let err = AiError::Validation("字段缺失".to_string());
|
||
let app: AppError = err.into();
|
||
match app {
|
||
AppError::Validation(msg) => assert!(msg.contains("字段缺失")),
|
||
other => panic!("期望 AppError::Validation,得到 {:?}", other),
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn analysis_not_found_maps_to_not_found() {
|
||
let err = AiError::AnalysisNotFound("abc-123".to_string());
|
||
let app: AppError = err.into();
|
||
match app {
|
||
AppError::NotFound(msg) => assert!(msg.contains("分析结果")),
|
||
other => panic!("期望 AppError::NotFound,得到 {:?}", other),
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn prompt_not_found_maps_to_not_found() {
|
||
let err = AiError::PromptNotFound("lab_report_interpretation".to_string());
|
||
let app: AppError = err.into();
|
||
match app {
|
||
AppError::NotFound(msg) => assert!(msg.contains("Prompt 模板")),
|
||
other => panic!("期望 AppError::NotFound,得到 {:?}", other),
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn provider_unavailable_maps_to_internal() {
|
||
let err = AiError::ProviderUnavailable("Claude".to_string());
|
||
let app: AppError = err.into();
|
||
match app {
|
||
AppError::Internal(msg) => assert!(msg.contains("Claude")),
|
||
other => panic!("期望 AppError::Internal,得到 {:?}", other),
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn provider_error_maps_to_internal() {
|
||
let err = AiError::ProviderError("超时".to_string());
|
||
let app: AppError = err.into();
|
||
match app {
|
||
AppError::Internal(msg) => assert!(msg.contains("超时")),
|
||
other => panic!("期望 AppError::Internal,得到 {:?}", other),
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn sanitization_error_maps_to_internal() {
|
||
let err = AiError::SanitizationError("PII 泄漏".to_string());
|
||
let app: AppError = err.into();
|
||
match app {
|
||
AppError::Internal(msg) => assert!(msg.contains("PII 泄漏")),
|
||
other => panic!("期望 AppError::Internal,得到 {:?}", other),
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn template_error_maps_to_internal() {
|
||
let err = AiError::TemplateError("语法错误".to_string());
|
||
let app: AppError = err.into();
|
||
match app {
|
||
AppError::Internal(msg) => assert!(msg.contains("语法错误")),
|
||
other => panic!("期望 AppError::Internal,得到 {:?}", other),
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn rate_limit_maps_to_too_many_requests() {
|
||
let err = AiError::RateLimitExceeded;
|
||
let app: AppError = err.into();
|
||
match app {
|
||
AppError::TooManyRequests => {}
|
||
other => panic!("期望 AppError::TooManyRequests,得到 {:?}", other),
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn version_mismatch_maps_directly() {
|
||
let err = AiError::VersionMismatch;
|
||
let app: AppError = err.into();
|
||
match app {
|
||
AppError::VersionMismatch => {}
|
||
other => panic!("期望 AppError::VersionMismatch,得到 {:?}", other),
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn db_error_maps_to_internal() {
|
||
let err = AiError::DbError("连接失败".to_string());
|
||
let app: AppError = err.into();
|
||
match app {
|
||
AppError::Internal(msg) => assert!(msg.contains("连接失败")),
|
||
other => panic!("期望 AppError::Internal,得到 {:?}", other),
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn quota_exhausted_maps_to_429() {
|
||
let err = AiError::QuotaExhausted {
|
||
tenant_id: uuid::Uuid::now_v7(),
|
||
reason: "monthly budget".into(),
|
||
};
|
||
let app: AppError = err.into();
|
||
assert!(matches!(app, AppError::TooManyRequests));
|
||
}
|
||
|
||
#[test]
|
||
fn cache_error_maps_to_internal() {
|
||
let err = AiError::CacheError("redis timeout".into());
|
||
let app: AppError = err.into();
|
||
match app {
|
||
AppError::Internal(msg) => assert!(msg.contains("redis timeout")),
|
||
other => panic!("期望 AppError::Internal,得到 {:?}", other),
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn knowledge_error_maps_to_internal() {
|
||
let err = AiError::KnowledgeError("rule not found".into());
|
||
let app: AppError = err.into();
|
||
match app {
|
||
AppError::Internal(msg) => assert!(msg.contains("rule not found")),
|
||
other => panic!("期望 AppError::Internal,得到 {:?}", other),
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn queue_error_maps_to_internal() {
|
||
let err = AiError::QueueError("queue full".into());
|
||
let app: AppError = err.into();
|
||
match app {
|
||
AppError::Internal(msg) => assert!(msg.contains("queue full")),
|
||
other => panic!("期望 AppError::Internal,得到 {:?}", other),
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn config_error_maps_to_internal() {
|
||
let err = AiError::ConfigError("missing provider".into());
|
||
let app: AppError = err.into();
|
||
match app {
|
||
AppError::Internal(msg) => assert!(msg.contains("missing provider")),
|
||
other => panic!("期望 AppError::Internal,得到 {:?}", other),
|
||
}
|
||
}
|
||
}
|