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

@@ -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)

View File

@@ -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");
}

View File

@@ -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)

View File

@@ -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))