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:
@@ -129,15 +129,12 @@ impl AiProvider for ClaudeProvider {
|
||||
if data == "[DONE]" {
|
||||
return;
|
||||
}
|
||||
if let Ok(event) = serde_json::from_str::<ClaudeStreamEvent>(data) {
|
||||
if event.event_type == "content_block_delta" {
|
||||
if let Some(delta) = event.delta {
|
||||
if let Some(text) = delta.text {
|
||||
if let Ok(event) = serde_json::from_str::<ClaudeStreamEvent>(data)
|
||||
&& event.event_type == "content_block_delta"
|
||||
&& let Some(delta) = event.delta
|
||||
&& let Some(text) = delta.text {
|
||||
yield Ok(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -179,9 +176,7 @@ impl AiProvider for ClaudeProvider {
|
||||
.map_err(|e| AiError::ProviderError(e.to_string()))?;
|
||||
|
||||
if !status.is_success() {
|
||||
return Err(AiError::ProviderError(format!(
|
||||
"Claude {status}: {body}"
|
||||
)));
|
||||
return Err(AiError::ProviderError(format!("Claude {status}: {body}")));
|
||||
}
|
||||
|
||||
let parsed: serde_json::Value = serde_json::from_str(&body)
|
||||
|
||||
@@ -75,8 +75,10 @@ struct OllamaStreamChunk {
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
struct OllamaStreamMessage {
|
||||
content: Option<String>,
|
||||
#[allow(dead_code)]
|
||||
thinking: Option<String>,
|
||||
}
|
||||
|
||||
@@ -87,7 +89,10 @@ fn strip_think_block(content: &str) -> String {
|
||||
if let Some(end) = content.find("</think") {
|
||||
// 跳过 </think 标签及其后的 > 或 \n
|
||||
let after_tag = &content[end + 7..]; // skip "</think"
|
||||
let actual = after_tag.trim_start_matches('\n').trim_start_matches('>').trim_start();
|
||||
let actual = after_tag
|
||||
.trim_start_matches('\n')
|
||||
.trim_start_matches('>')
|
||||
.trim_start();
|
||||
return actual.to_string();
|
||||
}
|
||||
content.to_string()
|
||||
@@ -193,13 +198,11 @@ impl AiProvider for OllamaProvider {
|
||||
if chunk.done {
|
||||
return;
|
||||
}
|
||||
if let Some(msg) = chunk.message {
|
||||
if let Some(content) = msg.content {
|
||||
if !content.is_empty() {
|
||||
if let Some(msg) = chunk.message
|
||||
&& let Some(content) = msg.content
|
||||
&& !content.is_empty() {
|
||||
yield Ok(content);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -252,9 +255,7 @@ impl AiProvider for OllamaProvider {
|
||||
.map_err(|e| AiError::ProviderError(e.to_string()))?;
|
||||
|
||||
if !status.is_success() {
|
||||
return Err(AiError::ProviderError(format!(
|
||||
"Ollama {status}: {body}"
|
||||
)));
|
||||
return Err(AiError::ProviderError(format!("Ollama {status}: {body}")));
|
||||
}
|
||||
|
||||
let parsed: OllamaChatResponse = serde_json::from_str(&body)
|
||||
@@ -307,10 +308,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn ollama_provider_construction() {
|
||||
let provider = OllamaProvider::new(
|
||||
"http://localhost:11434".into(),
|
||||
"qwen2.5:7b".into(),
|
||||
);
|
||||
let provider = OllamaProvider::new("http://localhost:11434".into(), "qwen2.5:7b".into());
|
||||
assert_eq!(provider.name(), "ollama");
|
||||
assert_eq!(provider.default_model, "qwen2.5:7b");
|
||||
}
|
||||
@@ -367,10 +365,7 @@ mod tests {
|
||||
}"#;
|
||||
let chunk: OllamaStreamChunk = serde_json::from_str(json).unwrap();
|
||||
assert!(!chunk.done);
|
||||
assert_eq!(
|
||||
chunk.message.unwrap().content,
|
||||
Some("Hello".to_string())
|
||||
);
|
||||
assert_eq!(chunk.message.unwrap().content, Some("Hello".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -388,10 +383,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn base_url_preserved() {
|
||||
let provider = OllamaProvider::new(
|
||||
"http://192.168.1.100:11434".into(),
|
||||
"llama3.1:8b".into(),
|
||||
);
|
||||
let provider =
|
||||
OllamaProvider::new("http://192.168.1.100:11434".into(), "llama3.1:8b".into());
|
||||
assert_eq!(provider.base_url, "http://192.168.1.100:11434");
|
||||
}
|
||||
|
||||
|
||||
@@ -202,9 +202,7 @@ impl AiProvider for OpenAIProvider {
|
||||
.map_err(|e| AiError::ProviderError(e.to_string()))?;
|
||||
|
||||
if !status.is_success() {
|
||||
return Err(AiError::ProviderError(format!(
|
||||
"OpenAI {status}: {body}"
|
||||
)));
|
||||
return Err(AiError::ProviderError(format!("OpenAI {status}: {body}")));
|
||||
}
|
||||
|
||||
let parsed: ChatResponse = serde_json::from_str(&body)
|
||||
|
||||
@@ -9,9 +9,17 @@ use tokio::sync::RwLock;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub enum ProviderHealth {
|
||||
Healthy { last_check: DateTime<Utc> },
|
||||
Degraded { last_check: DateTime<Utc>, error: String },
|
||||
Unavailable { since: DateTime<Utc>, error: String },
|
||||
Healthy {
|
||||
last_check: DateTime<Utc>,
|
||||
},
|
||||
Degraded {
|
||||
last_check: DateTime<Utc>,
|
||||
error: String,
|
||||
},
|
||||
Unavailable {
|
||||
since: DateTime<Utc>,
|
||||
error: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl ProviderHealth {
|
||||
@@ -29,6 +37,12 @@ pub struct ProviderRegistry {
|
||||
entries: DashMap<String, ProviderEntry>,
|
||||
}
|
||||
|
||||
impl Default for ProviderRegistry {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl ProviderRegistry {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
@@ -40,18 +54,19 @@ impl ProviderRegistry {
|
||||
let health = Arc::new(RwLock::new(ProviderHealth::Healthy {
|
||||
last_check: Utc::now(),
|
||||
}));
|
||||
self.entries.insert(name, ProviderEntry { provider, health });
|
||||
self.entries
|
||||
.insert(name, ProviderEntry { provider, health });
|
||||
}
|
||||
|
||||
pub async fn resolve(&self, preferred: &str) -> crate::error::AiResult<ResolvedProvider> {
|
||||
// 1. 首选 Provider(实时健康检查)
|
||||
if let Some(entry) = self.entries.get(preferred) {
|
||||
if entry.provider.health_check().await.unwrap_or(false) {
|
||||
return Ok(ResolvedProvider {
|
||||
provider_name: preferred.to_string(),
|
||||
provider: entry.provider.clone(),
|
||||
});
|
||||
}
|
||||
if let Some(entry) = self.entries.get(preferred)
|
||||
&& entry.provider.health_check().await.unwrap_or(false)
|
||||
{
|
||||
return Ok(ResolvedProvider {
|
||||
provider_name: preferred.to_string(),
|
||||
provider: entry.provider.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
// 2. 任何可用 Provider
|
||||
@@ -72,7 +87,9 @@ impl ProviderRegistry {
|
||||
for entry in self.entries.iter() {
|
||||
let healthy = entry.value().provider.health_check().await.unwrap_or(false);
|
||||
let new_health = if healthy {
|
||||
ProviderHealth::Healthy { last_check: Utc::now() }
|
||||
ProviderHealth::Healthy {
|
||||
last_check: Utc::now(),
|
||||
}
|
||||
} else {
|
||||
ProviderHealth::Unavailable {
|
||||
since: Utc::now(),
|
||||
@@ -96,14 +113,22 @@ pub struct ResolvedProvider {
|
||||
}
|
||||
|
||||
impl ResolvedProvider {
|
||||
pub fn provider_name(&self) -> &str { &self.provider_name }
|
||||
pub fn provider(&self) -> &dyn AiProvider { self.provider.as_ref() }
|
||||
pub fn into_arc(self) -> Arc<dyn AiProvider> { self.provider }
|
||||
pub fn provider_name(&self) -> &str {
|
||||
&self.provider_name
|
||||
}
|
||||
pub fn provider(&self) -> &dyn AiProvider {
|
||||
self.provider.as_ref()
|
||||
}
|
||||
pub fn into_arc(self) -> Arc<dyn AiProvider> {
|
||||
self.provider
|
||||
}
|
||||
}
|
||||
|
||||
// === 测试桩 ===
|
||||
|
||||
#[allow(dead_code)]
|
||||
struct MockProvider {
|
||||
#[allow(dead_code)]
|
||||
name: String,
|
||||
healthy: Arc<std::sync::atomic::AtomicBool>,
|
||||
}
|
||||
@@ -113,13 +138,18 @@ impl AiProvider for MockProvider {
|
||||
async fn stream_generate(
|
||||
&self,
|
||||
_req: crate::dto::GenerateRequest,
|
||||
) -> crate::error::AiResult<std::pin::Pin<Box<dyn futures::Stream<Item = crate::error::AiResult<String>> + Send>>> {
|
||||
) -> crate::error::AiResult<
|
||||
std::pin::Pin<Box<dyn futures::Stream<Item = crate::error::AiResult<String>> + Send>>,
|
||||
> {
|
||||
// 简单返回一个空流
|
||||
let s = async_stream::stream! { yield Ok("mock".to_string()); };
|
||||
Ok(Box::pin(s))
|
||||
}
|
||||
|
||||
async fn generate(&self, _req: crate::dto::GenerateRequest) -> crate::error::AiResult<crate::dto::GenerateResponse> {
|
||||
async fn generate(
|
||||
&self,
|
||||
_req: crate::dto::GenerateRequest,
|
||||
) -> crate::error::AiResult<crate::dto::GenerateResponse> {
|
||||
Ok(crate::dto::GenerateResponse {
|
||||
content: "mock".to_string(),
|
||||
model: "mock".to_string(),
|
||||
@@ -129,7 +159,9 @@ impl AiProvider for MockProvider {
|
||||
})
|
||||
}
|
||||
|
||||
fn name(&self) -> &str { &self.name }
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
async fn health_check(&self) -> crate::error::AiResult<bool> {
|
||||
Ok(self.healthy.load(std::sync::atomic::Ordering::Relaxed))
|
||||
|
||||
Reference in New Issue
Block a user