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:
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user