feat(health): 积分触发扩展 + 随访模板关联 — Phase 3
- 新增 follow_up.completed 事件积分消费者,随访完成触发 30 积分 - follow_up_task 新增 template_id FK 关联随访模板 - follow_up_record 新增 form_data JSONB 存储结构化表单数据 - 新增 POST /health/follow-up-tasks/from-template 基于模板创建随访任务端点 - 数据库迁移 160:follow_up_task.template_id + follow_up_record.form_data + 积分规则种子
This commit is contained in:
@@ -3,6 +3,7 @@ use erp_core::sanitize::sanitize_option;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use utoipa::{IntoParams, ToSchema};
|
use utoipa::{IntoParams, ToSchema};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
use validator::Validate;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, IntoParams)]
|
#[derive(Debug, Clone, Deserialize, IntoParams)]
|
||||||
pub struct FollowUpTaskListQuery {
|
pub struct FollowUpTaskListQuery {
|
||||||
@@ -25,6 +26,8 @@ pub struct CreateFollowUpTaskReq {
|
|||||||
pub source_type: Option<String>,
|
pub source_type: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub source_id: Option<Uuid>,
|
pub source_id: Option<Uuid>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub template_id: Option<Uuid>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CreateFollowUpTaskReq {
|
impl CreateFollowUpTaskReq {
|
||||||
@@ -65,6 +68,8 @@ pub struct FollowUpTaskResp {
|
|||||||
pub source_type: Option<String>,
|
pub source_type: Option<String>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub source_id: Option<Uuid>,
|
pub source_id: Option<Uuid>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub template_id: Option<Uuid>,
|
||||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||||
pub updated_at: chrono::DateTime<chrono::Utc>,
|
pub updated_at: chrono::DateTime<chrono::Utc>,
|
||||||
pub version: i32,
|
pub version: i32,
|
||||||
@@ -87,6 +92,8 @@ pub struct CreateFollowUpRecordReq {
|
|||||||
pub patient_condition: Option<String>,
|
pub patient_condition: Option<String>,
|
||||||
pub medical_advice: Option<String>,
|
pub medical_advice: Option<String>,
|
||||||
pub next_follow_up_date: Option<NaiveDate>,
|
pub next_follow_up_date: Option<NaiveDate>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub form_data: Option<serde_json::Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CreateFollowUpRecordReq {
|
impl CreateFollowUpRecordReq {
|
||||||
@@ -144,7 +151,23 @@ pub struct FollowUpRecordResp {
|
|||||||
pub patient_condition: Option<String>,
|
pub patient_condition: Option<String>,
|
||||||
pub medical_advice: Option<String>,
|
pub medical_advice: Option<String>,
|
||||||
pub next_follow_up_date: Option<NaiveDate>,
|
pub next_follow_up_date: Option<NaiveDate>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub form_data: Option<serde_json::Value>,
|
||||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||||
pub updated_at: chrono::DateTime<chrono::Utc>,
|
pub updated_at: chrono::DateTime<chrono::Utc>,
|
||||||
pub version: i32,
|
pub version: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 基于模板创建随访任务
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, Validate, utoipa::ToSchema)]
|
||||||
|
pub struct CreateTaskFromTemplateReq {
|
||||||
|
pub patient_id: Uuid,
|
||||||
|
pub template_id: Uuid,
|
||||||
|
pub planned_date: NaiveDate,
|
||||||
|
pub assigned_to: Option<Uuid>,
|
||||||
|
pub follow_up_type: Option<String>,
|
||||||
|
pub notes: Option<String>,
|
||||||
|
}
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ pub struct Model {
|
|||||||
pub version: i32,
|
pub version: i32,
|
||||||
#[sea_orm(skip_serializing_if = "Option::is_none")]
|
#[sea_orm(skip_serializing_if = "Option::is_none")]
|
||||||
pub key_version: Option<i32>,
|
pub key_version: Option<i32>,
|
||||||
|
#[sea_orm(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub form_data: Option<serde_json::Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ pub struct Model {
|
|||||||
pub source_type: Option<String>,
|
pub source_type: Option<String>,
|
||||||
#[sea_orm(skip_serializing_if = "Option::is_none")]
|
#[sea_orm(skip_serializing_if = "Option::is_none")]
|
||||||
pub source_id: Option<Uuid>,
|
pub source_id: Option<Uuid>,
|
||||||
|
#[sea_orm(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub template_id: Option<Uuid>,
|
||||||
pub created_at: DateTimeUtc,
|
pub created_at: DateTimeUtc,
|
||||||
pub updated_at: DateTimeUtc,
|
pub updated_at: DateTimeUtc,
|
||||||
#[sea_orm(skip_serializing_if = "Option::is_none")]
|
#[sea_orm(skip_serializing_if = "Option::is_none")]
|
||||||
|
|||||||
@@ -231,6 +231,7 @@ pub fn spawn(state: &crate::state::HealthState) -> Vec<erp_core::events::Subscri
|
|||||||
related_appointment_id: None,
|
related_appointment_id: None,
|
||||||
source_type: Some("critical_alert".to_string()),
|
source_type: Some("critical_alert".to_string()),
|
||||||
source_id: Some(event.id),
|
source_id: Some(event.id),
|
||||||
|
template_id: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
match crate::service::follow_up_service::create_task(
|
match crate::service::follow_up_service::create_task(
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
/// points.earned/exchanged/expired → 积分变动通知
|
/// points.earned/exchanged/expired → 积分变动通知
|
||||||
/// daily_monitoring.created → 健康数据上报积分
|
/// daily_monitoring.created → 健康数据上报积分
|
||||||
/// lab_report.uploaded → 化验报告上传积分
|
/// lab_report.uploaded → 化验报告上传积分
|
||||||
|
/// follow_up.completed → 随访完成积分
|
||||||
pub fn spawn(state: &crate::state::HealthState) -> Vec<erp_core::events::SubscriptionHandle> {
|
pub fn spawn(state: &crate::state::HealthState) -> Vec<erp_core::events::SubscriptionHandle> {
|
||||||
let mut handles = Vec::new();
|
let mut handles = Vec::new();
|
||||||
|
|
||||||
@@ -263,5 +264,73 @@ pub fn spawn(state: &crate::state::HealthState) -> Vec<erp_core::events::Subscri
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// follow_up.completed → 随访完成积分
|
||||||
|
let (mut fu_rx, fu_handle) = state.event_bus.subscribe_filtered("follow_up.".to_string());
|
||||||
|
handles.push(fu_handle);
|
||||||
|
let fu_state = state.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
loop {
|
||||||
|
match fu_rx.recv().await {
|
||||||
|
Some(event) if event.event_type == super::FOLLOW_UP_COMPLETED => {
|
||||||
|
if erp_core::events::is_event_processed(
|
||||||
|
&fu_state.db,
|
||||||
|
event.id,
|
||||||
|
"follow_up_points",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap_or(false)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let patient_id = event
|
||||||
|
.payload
|
||||||
|
.get("patient_id")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.and_then(|s| uuid::Uuid::parse_str(s).ok());
|
||||||
|
|
||||||
|
if let Some(pid) = patient_id {
|
||||||
|
match crate::service::points_service::earn_points(
|
||||||
|
&fu_state,
|
||||||
|
event.tenant_id,
|
||||||
|
pid,
|
||||||
|
"follow_up_completion",
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(tx) => {
|
||||||
|
tracing::info!(
|
||||||
|
patient_id = %pid,
|
||||||
|
points = tx.amount,
|
||||||
|
"随访完成积分已发放"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
let err_str = e.to_string();
|
||||||
|
if !err_str.contains("无匹配的积分规则") {
|
||||||
|
tracing::warn!(
|
||||||
|
patient_id = %pid,
|
||||||
|
error = %e,
|
||||||
|
"随访完成积分发放失败"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = erp_core::events::mark_event_processed(
|
||||||
|
&fu_state.db,
|
||||||
|
event.id,
|
||||||
|
"follow_up_points",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
Some(_) => {}
|
||||||
|
None => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
handles
|
handles
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use axum::extract::{FromRef, Json, Path, Query, State};
|
|||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use utoipa::IntoParams;
|
use utoipa::IntoParams;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
use validator::Validate;
|
||||||
|
|
||||||
use erp_core::error::AppError;
|
use erp_core::error::AppError;
|
||||||
use erp_core::rbac::require_permission;
|
use erp_core::rbac::require_permission;
|
||||||
@@ -250,3 +251,25 @@ where
|
|||||||
.await?;
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 基于模板创建随访任务
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
pub async fn create_task_from_template<S>(
|
||||||
|
State(state): State<HealthState>,
|
||||||
|
Extension(ctx): Extension<TenantContext>,
|
||||||
|
Json(req): Json<CreateTaskFromTemplateReq>,
|
||||||
|
) -> Result<Json<ApiResponse<FollowUpTaskResp>>, AppError>
|
||||||
|
where
|
||||||
|
HealthState: FromRef<S>,
|
||||||
|
S: Clone + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
require_permission(&ctx, "health.follow-up.manage")?;
|
||||||
|
req.validate()
|
||||||
|
.map_err(|e: validator::ValidationErrors| AppError::Validation(e.to_string()))?;
|
||||||
|
let result =
|
||||||
|
follow_up_service::create_task_from_template(&state, ctx.tenant_id, Some(ctx.user_id), req)
|
||||||
|
.await?;
|
||||||
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
|
}
|
||||||
|
|||||||
@@ -40,6 +40,10 @@ where
|
|||||||
axum::routing::get(follow_up_handler::list_records),
|
axum::routing::get(follow_up_handler::list_records),
|
||||||
)
|
)
|
||||||
// 随访批量操作
|
// 随访批量操作
|
||||||
|
.route(
|
||||||
|
"/health/follow-up-tasks/from-template",
|
||||||
|
axum::routing::post(follow_up_handler::create_task_from_template),
|
||||||
|
)
|
||||||
.route(
|
.route(
|
||||||
"/health/follow-up-tasks/batch-create",
|
"/health/follow-up-tasks/batch-create",
|
||||||
axum::routing::post(follow_up_handler::batch_create_tasks),
|
axum::routing::post(follow_up_handler::batch_create_tasks),
|
||||||
|
|||||||
@@ -900,6 +900,7 @@ pub async fn create_follow_up_from_session(
|
|||||||
related_appointment_id: None,
|
related_appointment_id: None,
|
||||||
source_type: Some("consultation".to_string()),
|
source_type: Some("consultation".to_string()),
|
||||||
source_id: Some(session_id),
|
source_id: Some(session_id),
|
||||||
|
template_id: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let task = crate::service::follow_up_service::create_task(
|
let task = crate::service::follow_up_service::create_task(
|
||||||
|
|||||||
@@ -131,6 +131,7 @@ pub async fn list_tasks(
|
|||||||
related_appointment_id: m.related_appointment_id,
|
related_appointment_id: m.related_appointment_id,
|
||||||
source_type: m.source_type,
|
source_type: m.source_type,
|
||||||
source_id: m.source_id,
|
source_id: m.source_id,
|
||||||
|
template_id: m.template_id,
|
||||||
created_at: m.created_at,
|
created_at: m.created_at,
|
||||||
updated_at: m.updated_at,
|
updated_at: m.updated_at,
|
||||||
version: m.version,
|
version: m.version,
|
||||||
@@ -173,6 +174,7 @@ pub async fn get_task(
|
|||||||
related_appointment_id: m.related_appointment_id,
|
related_appointment_id: m.related_appointment_id,
|
||||||
source_type: m.source_type,
|
source_type: m.source_type,
|
||||||
source_id: m.source_id,
|
source_id: m.source_id,
|
||||||
|
template_id: m.template_id,
|
||||||
created_at: m.created_at,
|
created_at: m.created_at,
|
||||||
updated_at: m.updated_at,
|
updated_at: m.updated_at,
|
||||||
version: m.version,
|
version: m.version,
|
||||||
@@ -211,6 +213,7 @@ pub async fn create_task(
|
|||||||
related_appointment_id: Set(req.related_appointment_id),
|
related_appointment_id: Set(req.related_appointment_id),
|
||||||
source_type: Set(req.source_type),
|
source_type: Set(req.source_type),
|
||||||
source_id: Set(req.source_id),
|
source_id: Set(req.source_id),
|
||||||
|
template_id: Set(req.template_id),
|
||||||
created_at: Set(now),
|
created_at: Set(now),
|
||||||
updated_at: Set(now),
|
updated_at: Set(now),
|
||||||
created_by: Set(operator_id),
|
created_by: Set(operator_id),
|
||||||
@@ -257,6 +260,7 @@ pub async fn create_task(
|
|||||||
related_appointment_id: m.related_appointment_id,
|
related_appointment_id: m.related_appointment_id,
|
||||||
source_type: m.source_type,
|
source_type: m.source_type,
|
||||||
source_id: m.source_id,
|
source_id: m.source_id,
|
||||||
|
template_id: m.template_id,
|
||||||
created_at: m.created_at,
|
created_at: m.created_at,
|
||||||
updated_at: m.updated_at,
|
updated_at: m.updated_at,
|
||||||
version: m.version,
|
version: m.version,
|
||||||
@@ -356,6 +360,7 @@ pub async fn update_task(
|
|||||||
related_appointment_id: m.related_appointment_id,
|
related_appointment_id: m.related_appointment_id,
|
||||||
source_type: m.source_type,
|
source_type: m.source_type,
|
||||||
source_id: m.source_id,
|
source_id: m.source_id,
|
||||||
|
template_id: m.template_id,
|
||||||
created_at: m.created_at,
|
created_at: m.created_at,
|
||||||
updated_at: m.updated_at,
|
updated_at: m.updated_at,
|
||||||
version: m.version,
|
version: m.version,
|
||||||
@@ -456,6 +461,7 @@ pub async fn batch_create_tasks(
|
|||||||
related_appointment_id: Set(None),
|
related_appointment_id: Set(None),
|
||||||
source_type: Set(None),
|
source_type: Set(None),
|
||||||
source_id: Set(None),
|
source_id: Set(None),
|
||||||
|
template_id: Set(None),
|
||||||
created_at: Set(now),
|
created_at: Set(now),
|
||||||
updated_at: Set(now),
|
updated_at: Set(now),
|
||||||
created_by: Set(operator_id),
|
created_by: Set(operator_id),
|
||||||
@@ -722,6 +728,7 @@ pub async fn create_record(
|
|||||||
deleted_at: Set(None),
|
deleted_at: Set(None),
|
||||||
version: Set(1),
|
version: Set(1),
|
||||||
key_version: Set(Some(1)),
|
key_version: Set(Some(1)),
|
||||||
|
form_data: Set(req.form_data),
|
||||||
};
|
};
|
||||||
let record = record_active.insert(&txn).await?;
|
let record = record_active.insert(&txn).await?;
|
||||||
|
|
||||||
@@ -750,6 +757,7 @@ pub async fn create_record(
|
|||||||
related_appointment_id: Set(None),
|
related_appointment_id: Set(None),
|
||||||
source_type: Set(None),
|
source_type: Set(None),
|
||||||
source_id: Set(None),
|
source_id: Set(None),
|
||||||
|
template_id: Set(None),
|
||||||
created_at: Set(now),
|
created_at: Set(now),
|
||||||
updated_at: Set(now),
|
updated_at: Set(now),
|
||||||
created_by: Set(operator_id),
|
created_by: Set(operator_id),
|
||||||
@@ -799,6 +807,7 @@ pub async fn create_record(
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|v| pii::decrypt(kek, v).unwrap_or(v.clone())),
|
.map(|v| pii::decrypt(kek, v).unwrap_or(v.clone())),
|
||||||
next_follow_up_date: record.next_follow_up_date,
|
next_follow_up_date: record.next_follow_up_date,
|
||||||
|
form_data: record.form_data,
|
||||||
created_at: record.created_at,
|
created_at: record.created_at,
|
||||||
updated_at: record.updated_at,
|
updated_at: record.updated_at,
|
||||||
version: record.version,
|
version: record.version,
|
||||||
@@ -873,6 +882,7 @@ pub async fn list_records(
|
|||||||
patient_condition,
|
patient_condition,
|
||||||
medical_advice,
|
medical_advice,
|
||||||
next_follow_up_date: m.next_follow_up_date,
|
next_follow_up_date: m.next_follow_up_date,
|
||||||
|
form_data: m.form_data,
|
||||||
created_at: m.created_at,
|
created_at: m.created_at,
|
||||||
updated_at: m.updated_at,
|
updated_at: m.updated_at,
|
||||||
version: m.version,
|
version: m.version,
|
||||||
@@ -889,6 +899,121 @@ pub async fn list_records(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 基于模板创建随访任务
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// 从随访模板创建随访任务。加载模板获取 follow_up_type,用模板名称作为 content_template。
|
||||||
|
pub async fn create_task_from_template(
|
||||||
|
state: &HealthState,
|
||||||
|
tenant_id: Uuid,
|
||||||
|
operator_id: Option<Uuid>,
|
||||||
|
req: CreateTaskFromTemplateReq,
|
||||||
|
) -> HealthResult<FollowUpTaskResp> {
|
||||||
|
tracing::info!(
|
||||||
|
action = "create_task_from_template",
|
||||||
|
patient_id = %req.patient_id,
|
||||||
|
template_id = %req.template_id,
|
||||||
|
"Creating follow-up task from template"
|
||||||
|
);
|
||||||
|
|
||||||
|
// 加载模板
|
||||||
|
let template = crate::entity::follow_up_template::Entity::find()
|
||||||
|
.filter(crate::entity::follow_up_template::Column::Id.eq(req.template_id))
|
||||||
|
.filter(crate::entity::follow_up_template::Column::TenantId.eq(tenant_id))
|
||||||
|
.filter(crate::entity::follow_up_template::Column::DeletedAt.is_null())
|
||||||
|
.one(&state.db)
|
||||||
|
.await?
|
||||||
|
.ok_or(HealthError::Validation("随访模板不存在".to_string()))?;
|
||||||
|
|
||||||
|
if template.status != "active" {
|
||||||
|
return Err(HealthError::Validation(
|
||||||
|
"随访模板未启用,无法创建任务".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确定随访类型:请求中显式指定 > 模板定义
|
||||||
|
let follow_up_type = req
|
||||||
|
.follow_up_type
|
||||||
|
.unwrap_or(template.follow_up_type.clone());
|
||||||
|
validate_follow_up_type(&follow_up_type)?;
|
||||||
|
|
||||||
|
// 校验患者存在
|
||||||
|
patient::Entity::find()
|
||||||
|
.filter(patient::Column::Id.eq(req.patient_id))
|
||||||
|
.filter(patient::Column::TenantId.eq(tenant_id))
|
||||||
|
.filter(patient::Column::DeletedAt.is_null())
|
||||||
|
.one(&state.db)
|
||||||
|
.await?
|
||||||
|
.ok_or(HealthError::PatientNotFound)?;
|
||||||
|
|
||||||
|
let now = Utc::now();
|
||||||
|
let active = follow_up_task::ActiveModel {
|
||||||
|
id: Set(Uuid::now_v7()),
|
||||||
|
tenant_id: Set(tenant_id),
|
||||||
|
patient_id: Set(req.patient_id),
|
||||||
|
assigned_to: Set(req.assigned_to),
|
||||||
|
follow_up_type: Set(follow_up_type),
|
||||||
|
planned_date: Set(req.planned_date),
|
||||||
|
status: Set("pending".to_string()),
|
||||||
|
content_template: Set(Some(template.name.clone())),
|
||||||
|
related_appointment_id: Set(None),
|
||||||
|
source_type: Set(Some("template".to_string())),
|
||||||
|
source_id: Set(Some(req.template_id)),
|
||||||
|
template_id: Set(Some(req.template_id)),
|
||||||
|
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),
|
||||||
|
};
|
||||||
|
let m = active.insert(&state.db).await?;
|
||||||
|
|
||||||
|
let event = DomainEvent::new(
|
||||||
|
crate::event::FOLLOW_UP_CREATED,
|
||||||
|
tenant_id,
|
||||||
|
erp_core::events::build_event_payload(serde_json::json!({
|
||||||
|
"task_id": m.id.to_string(),
|
||||||
|
"patient_id": m.patient_id.to_string(),
|
||||||
|
"template_id": req.template_id.to_string(),
|
||||||
|
"planned_date": m.planned_date.to_string(),
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
state.event_bus.publish(event, &state.db).await;
|
||||||
|
|
||||||
|
audit_service::record(
|
||||||
|
AuditLog::new(
|
||||||
|
tenant_id,
|
||||||
|
operator_id,
|
||||||
|
"follow_up_task.created_from_template",
|
||||||
|
"follow_up_task",
|
||||||
|
)
|
||||||
|
.with_resource_id(m.id),
|
||||||
|
&state.db,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
Ok(FollowUpTaskResp {
|
||||||
|
id: m.id,
|
||||||
|
patient_id: m.patient_id,
|
||||||
|
assigned_to: m.assigned_to,
|
||||||
|
patient_name: None,
|
||||||
|
assigned_to_name: None,
|
||||||
|
follow_up_type: m.follow_up_type,
|
||||||
|
planned_date: m.planned_date,
|
||||||
|
status: m.status,
|
||||||
|
content_template: m.content_template,
|
||||||
|
related_appointment_id: m.related_appointment_id,
|
||||||
|
source_type: m.source_type,
|
||||||
|
source_id: m.source_id,
|
||||||
|
template_id: m.template_id,
|
||||||
|
created_at: m.created_at,
|
||||||
|
updated_at: m.updated_at,
|
||||||
|
version: m.version,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// 随访任务状态机(委托给 validation 模块公共函数)
|
/// 随访任务状态机(委托给 validation 模块公共函数)
|
||||||
fn validate_follow_up_status_transition(current: &str, new_status: &str) -> HealthResult<()> {
|
fn validate_follow_up_status_transition(current: &str, new_status: &str) -> HealthResult<()> {
|
||||||
crate::service::validation::validate_follow_up_status_transition(current, new_status)
|
crate::service::validation::validate_follow_up_status_transition(current, new_status)
|
||||||
|
|||||||
@@ -159,6 +159,9 @@ mod m20260519_000154_seed_ai_knowledge_permissions;
|
|||||||
mod m20260519_000155_fix_ai_menus_and_add_chat;
|
mod m20260519_000155_fix_ai_menus_and_add_chat;
|
||||||
mod m20260519_000156_fix_ai_menus_round2;
|
mod m20260519_000156_fix_ai_menus_round2;
|
||||||
mod m20260520_000157_follow_up_source_and_points_rules;
|
mod m20260520_000157_follow_up_source_and_points_rules;
|
||||||
|
mod m20260521_000158_alerts_add_source_columns;
|
||||||
|
mod m20260521_000159_patient_phone_and_consent_seed;
|
||||||
|
mod m20260521_000160_follow_up_task_template_id_and_record_form_data;
|
||||||
|
|
||||||
pub struct Migrator;
|
pub struct Migrator;
|
||||||
|
|
||||||
@@ -325,6 +328,9 @@ impl MigratorTrait for Migrator {
|
|||||||
Box::new(m20260519_000155_fix_ai_menus_and_add_chat::Migration),
|
Box::new(m20260519_000155_fix_ai_menus_and_add_chat::Migration),
|
||||||
Box::new(m20260519_000156_fix_ai_menus_round2::Migration),
|
Box::new(m20260519_000156_fix_ai_menus_round2::Migration),
|
||||||
Box::new(m20260520_000157_follow_up_source_and_points_rules::Migration),
|
Box::new(m20260520_000157_follow_up_source_and_points_rules::Migration),
|
||||||
|
Box::new(m20260521_000158_alerts_add_source_columns::Migration),
|
||||||
|
Box::new(m20260521_000159_patient_phone_and_consent_seed::Migration),
|
||||||
|
Box::new(m20260521_000160_follow_up_task_template_id_and_record_form_data::Migration),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
use sea_orm_migration::prelude::*;
|
||||||
|
|
||||||
|
#[derive(DeriveMigrationName)]
|
||||||
|
pub struct Migration;
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl MigrationTrait for Migration {
|
||||||
|
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
// 1. follow_up_task 新增 template_id 字段,关联随访模板
|
||||||
|
manager
|
||||||
|
.alter_table(
|
||||||
|
Table::alter()
|
||||||
|
.table(Alias::new("follow_up_task"))
|
||||||
|
.add_column(ColumnDef::new(Alias::new("template_id")).uuid().null())
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// 2. follow_up_record 新增 form_data JSONB 字段,存储模板结构化表单数据
|
||||||
|
manager
|
||||||
|
.alter_table(
|
||||||
|
Table::alter()
|
||||||
|
.table(Alias::new("follow_up_record"))
|
||||||
|
.add_column(ColumnDef::new(Alias::new("form_data")).json_binary().null())
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// 3. 新增积分规则种子:随访完成
|
||||||
|
let insert_sql = r#"
|
||||||
|
INSERT INTO points_rule (id, tenant_id, name, event_type, points_value, daily_cap, is_active, created_at, updated_at, version)
|
||||||
|
SELECT
|
||||||
|
gen_random_uuid(),
|
||||||
|
t.id,
|
||||||
|
'随访完成',
|
||||||
|
'follow_up_completion',
|
||||||
|
30,
|
||||||
|
60,
|
||||||
|
true,
|
||||||
|
NOW(),
|
||||||
|
NOW(),
|
||||||
|
1
|
||||||
|
FROM tenant t
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM points_rule pr
|
||||||
|
WHERE pr.event_type = 'follow_up_completion' AND pr.tenant_id = t.id
|
||||||
|
);
|
||||||
|
"#;
|
||||||
|
manager
|
||||||
|
.get_connection()
|
||||||
|
.execute_unprepared(insert_sql)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
manager
|
||||||
|
.alter_table(
|
||||||
|
Table::alter()
|
||||||
|
.table(Alias::new("follow_up_task"))
|
||||||
|
.drop_column(Alias::new("template_id"))
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
manager
|
||||||
|
.alter_table(
|
||||||
|
Table::alter()
|
||||||
|
.table(Alias::new("follow_up_record"))
|
||||||
|
.drop_column(Alias::new("form_data"))
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
manager
|
||||||
|
.get_connection()
|
||||||
|
.execute_unprepared(
|
||||||
|
"DELETE FROM points_rule WHERE event_type = 'follow_up_completion';",
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user