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

@@ -252,7 +252,11 @@ mod tests {
for rt in &["user", "role", "department", "all"] {
let mut req = valid_send_message_req();
req.recipient_type = rt.to_string();
assert!(req.validate().is_ok(), "recipient_type '{}' should be valid", rt);
assert!(
req.validate().is_ok(),
"recipient_type '{}' should be valid",
rt
);
}
}

View File

@@ -6,8 +6,8 @@ use axum::extract::{Extension, Query};
use axum::http::HeaderMap;
use axum::response::sse::{Event, KeepAlive, Sse};
use futures::stream::Stream;
use serde::Deserialize;
use sea_orm::ConnectionTrait;
use serde::Deserialize;
use uuid::Uuid;
use erp_core::error::AppError;
@@ -73,12 +73,11 @@ pub async fn message_stream(
}
// Last-Event-ID 恢复:跳过已发送的事件
if let Some(skip_until) = last_event_id_cell.take() {
if event.id <= skip_until {
if let Some(skip_until) = last_event_id_cell.take()
&& event.id <= skip_until {
last_event_id_cell.set(Some(skip_until));
continue;
}
}
match event.event_type.as_str() {
"message.sent" => {
@@ -101,11 +100,10 @@ pub async fn message_stream(
.and_then(|v| v.as_str());
// 患者订阅过滤
if let (Some(pid_str), Some(subscribed)) = (patient_id, &subscribed_patient_ids) {
if !subscribed.contains(pid_str) {
if let (Some(pid_str), Some(subscribed)) = (patient_id, &subscribed_patient_ids)
&& !subscribed.contains(pid_str) {
continue;
}
}
if let Some(pid_str) = patient_id {
let pid = Uuid::parse_str(pid_str).ok();
@@ -130,11 +128,10 @@ pub async fn message_stream(
.and_then(|v| v.as_str());
// 患者订阅过滤
if let (Some(pid_str), Some(subscribed)) = (patient_id, &subscribed_patient_ids) {
if !subscribed.contains(pid_str) {
if let (Some(pid_str), Some(subscribed)) = (patient_id, &subscribed_patient_ids)
&& !subscribed.contains(pid_str) {
continue;
}
}
if let Some(pid_str) = patient_id {
let pid = Uuid::parse_str(pid_str).ok();
@@ -186,11 +183,7 @@ async fn is_doctor_for_patient(
sea_orm::DatabaseBackend::Postgres,
r#"SELECT COUNT(*) AS cnt FROM patient_doctor_relation
WHERE tenant_id = $1 AND doctor_id = $2 AND patient_id = $3 AND deleted_at IS NULL"#,
[
tenant_id.into(),
user_id.into(),
patient_id.into(),
],
[tenant_id.into(), user_id.into(), patient_id.into()],
);
match db.query_one(sql).await {
Ok(Some(row)) => {
@@ -251,7 +244,9 @@ mod tests {
#[test]
fn sse_query_parses_patient_ids() {
let query = SseQuery { patient_ids: Some("id1,id2,id3".into()) };
let query = SseQuery {
patient_ids: Some("id1,id2,id3".into()),
};
assert!(query.patient_ids.is_some());
let ids = query.patient_ids.unwrap();
assert_eq!(ids, "id1,id2,id3");
@@ -265,7 +260,9 @@ mod tests {
#[test]
fn subscribed_patient_ids_parsing() {
let query = SseQuery { patient_ids: Some("aaa,bbb,ccc".into()) };
let query = SseQuery {
patient_ids: Some("aaa,bbb,ccc".into()),
};
let set: Option<HashSet<String>> = query.patient_ids.map(|s: String| {
s.split(',')
.map(|id: &str| id.trim().to_string())

View File

@@ -74,12 +74,12 @@ impl MessageModule {
// 先获取许可,再 spawn 任务
tokio::spawn(async move {
let _permit = match permit.acquire().await {
Ok(p) => p,
Err(_) => {
tracing::warn!("信号量已关闭,跳过工作流事件处理");
return;
}
};
Ok(p) => p,
Err(_) => {
tracing::warn!("信号量已关闭,跳过工作流事件处理");
return;
}
};
if let Err(e) = handle_workflow_event(&event, &db, &event_bus).await {
tracing::warn!(
event_type = %event.event_type,
@@ -143,11 +143,36 @@ impl ErpModule for MessageModule {
fn permissions(&self) -> Vec<PermissionDescriptor> {
vec![
PermissionDescriptor { code: "message.list".into(), name: "查看消息".into(), description: "查看消息列表".into(), module: "message".into() },
PermissionDescriptor { code: "message.send".into(), name: "发送消息".into(), description: "发送新消息".into(), module: "message".into() },
PermissionDescriptor { code: "message.template.list".into(), name: "查看消息模板".into(), description: "查看消息模板列表".into(), module: "message".into() },
PermissionDescriptor { code: "message.template.create".into(), name: "创建消息模板".into(), description: "创建消息模板".into(), module: "message".into() },
PermissionDescriptor { code: "message.template.manage".into(), name: "管理消息模板".into(), description: "编辑、删除消息模板".into(), module: "message".into() },
PermissionDescriptor {
code: "message.list".into(),
name: "查看消息".into(),
description: "查看消息列表".into(),
module: "message".into(),
},
PermissionDescriptor {
code: "message.send".into(),
name: "发送消息".into(),
description: "发送新消息".into(),
module: "message".into(),
},
PermissionDescriptor {
code: "message.template.list".into(),
name: "查看消息模板".into(),
description: "查看消息模板列表".into(),
module: "message".into(),
},
PermissionDescriptor {
code: "message.template.create".into(),
name: "创建消息模板".into(),
description: "创建消息模板".into(),
module: "message".into(),
},
PermissionDescriptor {
code: "message.template.manage".into(),
name: "管理消息模板".into(),
description: "编辑、删除消息模板".into(),
module: "message".into(),
},
]
}
@@ -289,7 +314,10 @@ async fn handle_workflow_event(
event.tenant_id,
pid,
"预约已创建".to_string(),
format!("您的新预约 {} 已创建,请等待确认。", &appointment_id[..8.min(appointment_id.len())]),
format!(
"您的新预约 {} 已创建,请等待确认。",
&appointment_id[..8.min(appointment_id.len())]
),
"normal",
Some("appointment".to_string()),
uuid::Uuid::parse_str(appointment_id).ok(),
@@ -361,7 +389,10 @@ async fn handle_workflow_event(
event.tenant_id,
pid,
"预约已取消".to_string(),
format!("您的预约 {} 已被取消。", &appointment_id[..8.min(appointment_id.len())]),
format!(
"您的预约 {} 已被取消。",
&appointment_id[..8.min(appointment_id.len())]
),
"normal",
Some("appointment".to_string()),
uuid::Uuid::parse_str(appointment_id).ok(),
@@ -397,10 +428,17 @@ async fn handle_workflow_event(
event.tenant_id,
pid,
"预约提醒".to_string(),
format!("您明天({})有一个预约,时间段:{},请准时就诊。", appointment_date, time_slot),
format!(
"您明天({})有一个预约,时间段:{},请准时就诊。",
appointment_date, time_slot
),
"normal",
Some("appointment".to_string()),
event.payload.get("appointment_id").and_then(|v| v.as_str()).and_then(|s| uuid::Uuid::parse_str(s).ok()),
event
.payload
.get("appointment_id")
.and_then(|v| v.as_str())
.and_then(|s| uuid::Uuid::parse_str(s).ok()),
db,
event_bus,
)
@@ -723,7 +761,10 @@ async fn handle_workflow_event(
event.tenant_id,
assignee,
format!("新随访任务{}", patient_info),
format!("您被分配了一个随访任务{},计划日期:{}", patient_info, planned_date),
format!(
"您被分配了一个随访任务{},计划日期:{}",
patient_info, planned_date
),
"normal",
Some("follow_up".to_string()),
uuid::Uuid::parse_str(task_id).ok(),
@@ -1005,10 +1046,15 @@ async fn handle_workflow_event(
event.tenant_id,
pid,
"护理计划已完成".to_string(),
"您的护理计划已完成,感谢您这段时间的配合!我们将继续关注您的健康。".to_string(),
"您的护理计划已完成,感谢您这段时间的配合!我们将继续关注您的健康。"
.to_string(),
"normal",
Some("care_plan".to_string()),
event.payload.get("plan_id").and_then(|v| v.as_str()).and_then(|s| uuid::Uuid::parse_str(s).ok()),
event
.payload
.get("plan_id")
.and_then(|v| v.as_str())
.and_then(|s| uuid::Uuid::parse_str(s).ok()),
db,
event_bus,
)
@@ -1036,25 +1082,31 @@ async fn handle_workflow_event(
let (title, body) = match action {
"item_completed" => {
let item_title = event.payload.get("item_title").and_then(|v| v.as_str()).unwrap_or("护理项目");
let item_title = event
.payload
.get("item_title")
.and_then(|v| v.as_str())
.unwrap_or("护理项目");
(
"关怀已送达".to_string(),
format!("您的护理团队已完成「{}」,感谢您的配合。", item_title),
)
}
"outcome_measured" => {
let metric = event.payload.get("metric").and_then(|v| v.as_str()).unwrap_or("健康指标");
let metric = event
.payload
.get("metric")
.and_then(|v| v.as_str())
.unwrap_or("健康指标");
(
"健康数据已更新".to_string(),
format!("您的{}数据已记录,护理团队正在持续关注。", metric),
)
}
_ => {
(
"关怀已送达".to_string(),
"您的护理团队正在关注您的健康状况。".to_string(),
)
}
_ => (
"关怀已送达".to_string(),
"您的护理团队正在关注您的健康状况。".to_string(),
),
};
let _ = crate::service::message_service::MessageService::send_system(
@@ -1064,7 +1116,11 @@ async fn handle_workflow_event(
body,
"low",
Some("care_action".to_string()),
event.payload.get("plan_id").and_then(|v| v.as_str()).and_then(|s| uuid::Uuid::parse_str(s).ok()),
event
.payload
.get("plan_id")
.and_then(|v| v.as_str())
.and_then(|s| uuid::Uuid::parse_str(s).ok()),
db,
event_bus,
)

View File

@@ -1,7 +1,7 @@
use chrono::Utc;
use sea_orm::{
ActiveModelTrait, ColumnTrait, ConnectionTrait, DatabaseBackend, EntityTrait, PaginatorTrait,
QueryFilter, Set, Statement, FromQueryResult,
ActiveModelTrait, ColumnTrait, ConnectionTrait, DatabaseBackend, EntityTrait, FromQueryResult,
PaginatorTrait, QueryFilter, Set, Statement,
};
use uuid::Uuid;
@@ -104,15 +104,11 @@ impl MessageService {
// Resolve target user IDs based on recipient type
let recipient_user_ids = match req.recipient_type.as_str() {
"user" => vec![req.recipient_id],
"role" => {
Self::resolve_user_ids_by_role(db, req.recipient_id, tenant_id).await?
}
"role" => Self::resolve_user_ids_by_role(db, req.recipient_id, tenant_id).await?,
"department" => {
Self::resolve_user_ids_by_department(db, req.recipient_id, tenant_id).await?
}
"all" => {
Self::resolve_all_active_user_ids(db, tenant_id).await?
}
"all" => Self::resolve_all_active_user_ids(db, tenant_id).await?,
other => {
return Err(MessageError::Validation(format!(
"不支持的收件人类型: {other}"
@@ -180,15 +176,14 @@ impl MessageService {
.await;
audit_service::record(
AuditLog::new(tenant_id, Some(sender_id), "message.send", "message")
.with_changes(
None,
Some(serde_json::json!({
"recipient_type": req.recipient_type,
"recipient_count": recipient_user_ids.len(),
"title": req.title,
})),
),
AuditLog::new(tenant_id, Some(sender_id), "message.send", "message").with_changes(
None,
Some(serde_json::json!({
"recipient_type": req.recipient_type,
"recipient_count": recipient_user_ids.len(),
"title": req.title,
})),
),
db,
)
.await;