feat(health): 批量随访操作 — batch_create/assign/complete 三个端点
POST /health/follow-up-tasks/batch-create — 多患者同配置批量创建 POST /health/follow-up-tasks/batch-assign — 批量分配负责人 POST /health/follow-up-tasks/batch-complete — 批量标记完成 含参数校验(上限 100)、部分失败报告、事件发布、审计日志。
This commit is contained in:
@@ -88,6 +88,44 @@ impl CreateFollowUpRecordReq {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 批量操作
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct BatchCreateTasksReq {
|
||||
pub patient_ids: Vec<Uuid>,
|
||||
pub assigned_to: Option<Uuid>,
|
||||
pub follow_up_type: String,
|
||||
pub planned_date: NaiveDate,
|
||||
pub content_template: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct BatchAssignReq {
|
||||
pub task_ids: Vec<Uuid>,
|
||||
pub assigned_to: Uuid,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct BatchCompleteReq {
|
||||
pub task_ids: Vec<Uuid>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct BatchResultResp {
|
||||
pub succeeded: u32,
|
||||
pub failed: u32,
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
pub errors: Vec<BatchError>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct BatchError {
|
||||
pub index: usize,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct FollowUpRecordResp {
|
||||
pub id: Uuid,
|
||||
|
||||
@@ -13,6 +13,79 @@ use crate::dto::DeleteWithVersion;
|
||||
use crate::service::follow_up_service;
|
||||
use crate::state::HealthState;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 批量操作 Handler
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub async fn batch_create_tasks<S>(
|
||||
State(state): State<HealthState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Json(req): Json<BatchCreateTasksReq>,
|
||||
) -> Result<Json<ApiResponse<BatchResultResp>>, AppError>
|
||||
where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.follow-up.manage")?;
|
||||
if req.patient_ids.is_empty() {
|
||||
return Err(AppError::Validation("patient_ids 不能为空".to_string()));
|
||||
}
|
||||
if req.patient_ids.len() > 100 {
|
||||
return Err(AppError::Validation("单次批量最多 100 条".to_string()));
|
||||
}
|
||||
let result = follow_up_service::batch_create_tasks(
|
||||
&state, ctx.tenant_id, Some(ctx.user_id), req,
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(ApiResponse::ok(result)))
|
||||
}
|
||||
|
||||
pub async fn batch_assign_tasks<S>(
|
||||
State(state): State<HealthState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Json(req): Json<BatchAssignReq>,
|
||||
) -> Result<Json<ApiResponse<BatchResultResp>>, AppError>
|
||||
where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.follow-up.manage")?;
|
||||
if req.task_ids.is_empty() {
|
||||
return Err(AppError::Validation("task_ids 不能为空".to_string()));
|
||||
}
|
||||
if req.task_ids.len() > 100 {
|
||||
return Err(AppError::Validation("单次批量最多 100 条".to_string()));
|
||||
}
|
||||
let result = follow_up_service::batch_assign_tasks(
|
||||
&state, ctx.tenant_id, Some(ctx.user_id), req,
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(ApiResponse::ok(result)))
|
||||
}
|
||||
|
||||
pub async fn batch_complete_tasks<S>(
|
||||
State(state): State<HealthState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Json(req): Json<BatchCompleteReq>,
|
||||
) -> Result<Json<ApiResponse<BatchResultResp>>, AppError>
|
||||
where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.follow-up.manage")?;
|
||||
if req.task_ids.is_empty() {
|
||||
return Err(AppError::Validation("task_ids 不能为空".to_string()));
|
||||
}
|
||||
if req.task_ids.len() > 100 {
|
||||
return Err(AppError::Validation("单次批量最多 100 条".to_string()));
|
||||
}
|
||||
let result = follow_up_service::batch_complete_tasks(
|
||||
&state, ctx.tenant_id, Some(ctx.user_id), req,
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(ApiResponse::ok(result)))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct FollowUpTaskListParams {
|
||||
pub page: Option<u64>,
|
||||
|
||||
@@ -289,6 +289,19 @@ impl HealthModule {
|
||||
"/health/follow-up-records",
|
||||
axum::routing::get(follow_up_handler::list_records),
|
||||
)
|
||||
// 随访批量操作
|
||||
.route(
|
||||
"/health/follow-up-tasks/batch-create",
|
||||
axum::routing::post(follow_up_handler::batch_create_tasks),
|
||||
)
|
||||
.route(
|
||||
"/health/follow-up-tasks/batch-assign",
|
||||
axum::routing::post(follow_up_handler::batch_assign_tasks),
|
||||
)
|
||||
.route(
|
||||
"/health/follow-up-tasks/batch-complete",
|
||||
axum::routing::post(follow_up_handler::batch_complete_tasks),
|
||||
)
|
||||
// 咨询管理
|
||||
.route(
|
||||
"/health/consultation-sessions",
|
||||
|
||||
@@ -292,6 +292,220 @@ pub async fn delete_task(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 批量操作
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub async fn batch_create_tasks(
|
||||
state: &HealthState,
|
||||
tenant_id: Uuid,
|
||||
operator_id: Option<Uuid>,
|
||||
req: BatchCreateTasksReq,
|
||||
) -> HealthResult<BatchResultResp> {
|
||||
validate_follow_up_type(&req.follow_up_type)?;
|
||||
let mut succeeded: u32 = 0;
|
||||
let mut errors: Vec<crate::dto::follow_up_dto::BatchError> = Vec::new();
|
||||
|
||||
// 批量校验患者存在
|
||||
let valid_ids: HashSet<Uuid> = patient::Entity::find()
|
||||
.filter(patient::Column::Id.is_in(req.patient_ids.clone()))
|
||||
.filter(patient::Column::TenantId.eq(tenant_id))
|
||||
.filter(patient::Column::DeletedAt.is_null())
|
||||
.all(&state.db)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|p| p.id)
|
||||
.collect();
|
||||
|
||||
let now = Utc::now();
|
||||
let mut models_to_insert = Vec::new();
|
||||
|
||||
for (i, pid) in req.patient_ids.iter().enumerate() {
|
||||
if !valid_ids.contains(pid) {
|
||||
errors.push(crate::dto::follow_up_dto::BatchError {
|
||||
index: i,
|
||||
message: format!("患者 {} 不存在", pid),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
models_to_insert.push(follow_up_task::ActiveModel {
|
||||
id: Set(Uuid::now_v7()),
|
||||
tenant_id: Set(tenant_id),
|
||||
patient_id: Set(*pid),
|
||||
assigned_to: Set(req.assigned_to),
|
||||
follow_up_type: Set(req.follow_up_type.clone()),
|
||||
planned_date: Set(req.planned_date),
|
||||
status: Set("pending".to_string()),
|
||||
content_template: Set(req.content_template.clone()),
|
||||
related_appointment_id: Set(None),
|
||||
created_at: Set(now),
|
||||
updated_at: Set(now),
|
||||
created_by: Set(operator_id),
|
||||
updated_by: Set(operator_id),
|
||||
deleted_at: Set(None),
|
||||
version: Set(1),
|
||||
});
|
||||
}
|
||||
|
||||
if !models_to_insert.is_empty() {
|
||||
let count = models_to_insert.len() as u32;
|
||||
follow_up_task::Entity::insert_many(models_to_insert)
|
||||
.exec(&state.db)
|
||||
.await
|
||||
.map_err(|e| HealthError::DbError(e.to_string()))?;
|
||||
|
||||
// 发布聚合事件(而非逐条发布)
|
||||
state.event_bus.publish(
|
||||
DomainEvent::new(
|
||||
crate::event::FOLLOW_UP_CREATED,
|
||||
tenant_id,
|
||||
serde_json::json!({ "batch_count": count, "assigned_to": req.assigned_to }),
|
||||
),
|
||||
&state.db,
|
||||
).await;
|
||||
|
||||
audit_service::record(
|
||||
AuditLog::new(tenant_id, operator_id, "follow_up_task.batch_created", "follow_up_task")
|
||||
.with_resource_id(req.patient_ids[0]),
|
||||
&state.db,
|
||||
).await;
|
||||
|
||||
succeeded = count;
|
||||
}
|
||||
|
||||
Ok(BatchResultResp {
|
||||
succeeded,
|
||||
failed: errors.len() as u32,
|
||||
errors,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn batch_assign_tasks(
|
||||
state: &HealthState,
|
||||
tenant_id: Uuid,
|
||||
operator_id: Option<Uuid>,
|
||||
req: BatchAssignReq,
|
||||
) -> HealthResult<BatchResultResp> {
|
||||
let mut succeeded: u32 = 0;
|
||||
let mut errors: Vec<crate::dto::follow_up_dto::BatchError> = Vec::new();
|
||||
|
||||
let tasks: Vec<follow_up_task::Model> = follow_up_task::Entity::find()
|
||||
.filter(follow_up_task::Column::Id.is_in(req.task_ids.clone()))
|
||||
.filter(follow_up_task::Column::TenantId.eq(tenant_id))
|
||||
.filter(follow_up_task::Column::DeletedAt.is_null())
|
||||
.all(&state.db)
|
||||
.await?;
|
||||
|
||||
let found_ids: HashSet<Uuid> = tasks.iter().map(|t| t.id).collect();
|
||||
|
||||
for (i, tid) in req.task_ids.iter().enumerate() {
|
||||
if !found_ids.contains(tid) {
|
||||
errors.push(crate::dto::follow_up_dto::BatchError {
|
||||
index: i,
|
||||
message: format!("任务 {} 不存在", tid),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if found_ids.is_empty() {
|
||||
return Ok(BatchResultResp { succeeded: 0, failed: errors.len() as u32, errors });
|
||||
}
|
||||
|
||||
// 批量更新 assigned_to
|
||||
let now = Utc::now();
|
||||
for task in tasks {
|
||||
let mut active: follow_up_task::ActiveModel = task.into();
|
||||
active.assigned_to = Set(Some(req.assigned_to));
|
||||
active.updated_at = Set(now);
|
||||
active.updated_by = Set(operator_id);
|
||||
active.version = Set(active.version.unwrap() + 1);
|
||||
active.update(&state.db).await.map_err(|e| HealthError::DbError(e.to_string()))?;
|
||||
succeeded += 1;
|
||||
}
|
||||
|
||||
audit_service::record(
|
||||
AuditLog::new(tenant_id, operator_id, "follow_up_task.batch_assigned", "follow_up_task")
|
||||
.with_resource_id(req.assigned_to),
|
||||
&state.db,
|
||||
).await;
|
||||
|
||||
Ok(BatchResultResp {
|
||||
succeeded,
|
||||
failed: errors.len() as u32,
|
||||
errors,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn batch_complete_tasks(
|
||||
state: &HealthState,
|
||||
tenant_id: Uuid,
|
||||
operator_id: Option<Uuid>,
|
||||
req: BatchCompleteReq,
|
||||
) -> HealthResult<BatchResultResp> {
|
||||
let mut succeeded: u32 = 0;
|
||||
let mut errors: Vec<crate::dto::follow_up_dto::BatchError> = Vec::new();
|
||||
|
||||
let task_id_refs: Vec<Uuid> = req.task_ids.clone();
|
||||
let tasks: Vec<follow_up_task::Model> = follow_up_task::Entity::find()
|
||||
.filter(follow_up_task::Column::Id.is_in(task_id_refs))
|
||||
.filter(follow_up_task::Column::TenantId.eq(tenant_id))
|
||||
.filter(follow_up_task::Column::DeletedAt.is_null())
|
||||
.all(&state.db)
|
||||
.await?;
|
||||
|
||||
let found_map: HashMap<Uuid, follow_up_task::Model> =
|
||||
tasks.into_iter().map(|t| (t.id, t)).collect();
|
||||
|
||||
let now = Utc::now();
|
||||
for (i, tid) in req.task_ids.iter().enumerate() {
|
||||
if let Some(task) = found_map.get(tid) {
|
||||
if task.status != "pending" && task.status != "in_progress" {
|
||||
errors.push(crate::dto::follow_up_dto::BatchError {
|
||||
index: i,
|
||||
message: format!("任务状态为 {},无法完成", task.status),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
let next_ver = task.version + 1;
|
||||
let mut active: follow_up_task::ActiveModel = task.clone().into();
|
||||
active.status = Set("completed".to_string());
|
||||
active.updated_at = Set(now);
|
||||
active.updated_by = Set(operator_id);
|
||||
active.version = Set(next_ver);
|
||||
active.update(&state.db).await.map_err(|e| HealthError::DbError(e.to_string()))?;
|
||||
succeeded += 1;
|
||||
} else {
|
||||
errors.push(crate::dto::follow_up_dto::BatchError {
|
||||
index: i,
|
||||
message: format!("任务 {} 不存在", tid),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if succeeded > 0 {
|
||||
state.event_bus.publish(
|
||||
DomainEvent::new(
|
||||
crate::event::FOLLOW_UP_COMPLETED,
|
||||
tenant_id,
|
||||
serde_json::json!({ "batch_count": succeeded }),
|
||||
),
|
||||
&state.db,
|
||||
).await;
|
||||
|
||||
audit_service::record(
|
||||
AuditLog::new(tenant_id, operator_id, "follow_up_task.batch_completed", "follow_up_task")
|
||||
.with_resource_id(req.task_ids[0]),
|
||||
&state.db,
|
||||
).await;
|
||||
}
|
||||
|
||||
Ok(BatchResultResp {
|
||||
succeeded,
|
||||
failed: errors.len() as u32,
|
||||
errors,
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 随访记录
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user