Files
hms/crates/erp-ai/src/error.rs
iven 6d5a711d2c
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
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 统一格式化
2026-05-07 23:43:14 +08:00

232 lines
7.2 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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),
}
}
}